1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
use alloc::{
boxed::Box,
format,
};
use anyhow::Result;
use battler_prng::{
PseudoRandomNumberGenerator,
RealPseudoRandomNumberGenerator,
};
use serde::{
Deserialize,
Serialize,
};
use crate::{
battle::{
BattleType,
FieldData,
PlayerData,
SideData,
},
common::Clock,
config::FormatData,
error::general_error,
};
/// Battle engine option for how base damage should be randomized in the damage calculation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CoreBattleEngineRandomizeBaseDamage {
/// Randomize the base damage.
///
/// This is the default behavior.
Randomize,
/// Only use the maximum base damage value.
Max,
/// Only use the minimum base damage value.
Min,
}
/// How the battle engine should resolve ties when sorting by speed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CoreBattleEngineSpeedSortTieResolution {
/// Resolves ties randomly by advancing RNG.
Random,
/// Do not resolve ties and keep the original order of tied elements.
Keep,
/// Reverse the original order of tied elements.
Reverse,
}
fn default_rng_factory() -> fn(seed: Option<u64>) -> Box<dyn PseudoRandomNumberGenerator> {
|seed: Option<u64>| Box::new(RealPseudoRandomNumberGenerator::new(seed))
}
fn default_clock_factory() -> Option<fn() -> Box<dyn Clock>> {
#[cfg(feature = "std")]
{
return Some(|| Box::new(crate::common::SystemTimeClock));
}
#[allow(unused)]
None
}
fn default_true() -> bool {
true
}
fn default_100() -> u32 {
100
}
fn default_randomize() -> CoreBattleEngineRandomizeBaseDamage {
CoreBattleEngineRandomizeBaseDamage::Randomize
}
fn default_random() -> CoreBattleEngineSpeedSortTieResolution {
CoreBattleEngineSpeedSortTieResolution::Random
}
/// Options that change how the battle engine itself behaves, which is not necessarily specific to
/// any individual battle.
///
/// Options defined here relate to how the battle engine is operated, so it is likely that these
/// options will be common across all battle instances.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoreBattleEngineOptions {
/// Should all teams be validated prior to the battle being able to start?
#[serde(default = "default_true")]
pub validate_teams: bool,
/// Should the [`CoreBattle`][`crate::battle::CoreBattle`] automatically continue when it is
/// able to?
///
/// If set to `true`, a [`CoreBattle`][`crate::battle::CoreBattle`] object will continue the
/// battle as soon as it finds that it is able to. The best example of this is when a
/// player makes a choice: if all players have made responded to their request, then the
/// battle can automatically continue in the same method as the last player's choice.
///
/// If set to `false`,
/// [`PublicCoreBattle::continue_battle`][`crate::battle::PublicCoreBattle::continue_battle`]
/// must be called to manually continue the battle (even at the start of the battle).
#[serde(default = "default_true")]
pub auto_continue: bool,
/// Should the [`CoreBattle`][`crate::battle::CoreBattle`] reveal the actual health of all Mons
/// in the public battle log?
///
/// By default, the public log will show the health of all Mons as a percentage (fraction out
/// of 100). If this option is set to `true`, the battle will show the actual HP stat of each
/// Mon.
#[serde(default)]
pub reveal_actual_health: bool,
/// A custom denominator for public health logs.
///
/// By default, public health is revealed as a percentage out of 100. A different base can be
/// used for higher or lower precision.
#[serde(default = "default_100")]
pub public_health_base: u32,
/// Function for creating the battle's random number generator.
///
/// Primarily useful for tests where we wish to have fine-grained control over battle RNG.
#[serde(skip, default = "default_rng_factory")]
pub rng_factory: fn(seed: Option<u64>) -> Box<dyn PseudoRandomNumberGenerator>,
#[serde(skip, default = "default_clock_factory")]
pub clock_factory: Option<fn() -> Box<dyn Clock>>,
/// Are players allowed to pass for unfainted Mons?
///
/// By default, "pass" actions are forced when the player does not have enough Mons to fulfill
/// all requirements. For example if a player has a team of 3 Mons for a doubles battle and 2
/// faint at the same time, the player will is allowed to send one "switch" action and the
/// other is forced to be a pass
///
/// In all other cases, players cannot instruct their Mons to pass at the beginning of a turn.
/// This prevents battles from getting into a stalemate position forever.
///
/// If this property is set to `true`, players will be allowed to send "pass" actions. This is
/// mostly useful for tests where we want to control one side while the other side sits
/// passively.
#[serde(default)]
pub allow_pass_for_unfainted_mon: bool,
/// Describes how base damage should be randomized in the damage calculation.
///
/// By default, base damage is randomized early in the damage calculation. This property can
/// control how the damage should be randomized. This is useful for tests against the damage
/// calculator to discover the minimum and maximum damage values.
#[serde(default = "default_randomize")]
pub randomize_base_damage: CoreBattleEngineRandomizeBaseDamage,
/// Describes how ties should be resolved when sorting elements by speed.
///
/// By default, speed ties are resolved randomly. However, many tests involve a lot of speed
/// ties, complicating test results when RNG shifts slightly. This property can be used to
/// avoid using RNG in speed sorting completely.
#[serde(default = "default_random")]
pub speed_sort_tie_resolution: CoreBattleEngineSpeedSortTieResolution,
/// Should volatile statuses be logged?
///
/// By default, volatile statuses are invisible to Mons, since they are used to implement
/// complex interactions in the battle system. It may be helpful, especially for debugging
/// purposes, to view all volatile statuses added to and removed from Mons through the course
/// of a battle.
#[serde(default)]
pub log_volatile_statuses: bool,
/// Should side conditions be logged?
///
/// By default, side conditions are invisible to Mons unless explicitly logged. It may be
/// helpful, especially for debugging purposes, to view all side conditions added to and
/// removed from sides through the course of a battle.
#[serde(default)]
pub log_side_conditions: bool,
/// Should slot conditions be logged?
///
/// By default, slot conditions are invisible to Mons unless explicitly logged. It may be
/// helpful, especially for debugging purposes, to view all slot conditions added to and
/// removed from sides through the course of a battle.
#[serde(default)]
pub log_slot_conditions: bool,
/// Should pseudo-weathers be logged?
///
/// By default, pseudo-weather conditions are invisible to Mons unless explicitly logged. It
/// may be helpful, especially for debugging purposes, to view all pseudo-weathers added to
/// and removed from the field through the course of a battle.
#[serde(default)]
pub log_pseudo_weathers: bool,
/// Should identical Mon names for a single player be disambiguated?
///
/// If set to true, Mons with the same name for a single player will have a disambiguation
/// string appended to their name of the pattern `###N` (where `N` is a number). Clients can
/// simply strip off this string when displaying the Mon name.
#[serde(default)]
pub disambiguate_identical_names: bool,
/// Should catch rates and shake probabilities be logged?
///
/// Helpful for debugging.
#[serde(default)]
pub log_catch_rate: bool,
/// Should the time be logged each time the battle continues?
///
/// If false, a `continue` log will be added instead.
#[serde(default = "default_true")]
pub log_time: bool,
}
impl Default for CoreBattleEngineOptions {
fn default() -> Self {
Self {
validate_teams: true,
auto_continue: true,
reveal_actual_health: false,
public_health_base: 100,
rng_factory: default_rng_factory(),
clock_factory: default_clock_factory(),
allow_pass_for_unfainted_mon: false,
randomize_base_damage: CoreBattleEngineRandomizeBaseDamage::Randomize,
speed_sort_tie_resolution: CoreBattleEngineSpeedSortTieResolution::Random,
log_volatile_statuses: false,
log_side_conditions: false,
log_slot_conditions: false,
log_pseudo_weathers: false,
disambiguate_identical_names: false,
log_catch_rate: false,
log_time: true,
}
}
}
/// Core options for a new battle.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct CoreBattleOptions {
/// The initial seed for random number generation.
///
/// This can be used to effectively replay or control a battle.
pub seed: Option<u64>,
/// The format of the battle.
pub format: FormatData,
/// The field of the battle.
#[serde(default)]
pub field: FieldData,
/// One side of the battle.
pub side_1: SideData,
/// The other side of the battle.
pub side_2: SideData,
}
impl CoreBattleOptions {
fn validate_side(&self, side: &SideData) -> Result<()> {
let players_on_side = side.players.len();
if players_on_side == 0 {
return Err(general_error(format!("{} has no players", side.name)));
}
match self.format.battle_type {
BattleType::Singles => {
if players_on_side > 1 {
return Err(general_error(format!(
"{} has too many players for a singles battle",
side.name
)));
}
}
BattleType::Doubles => {
if players_on_side > 1 {
return Err(general_error(format!(
"{} has too many players for a doubles battle (did you mean to start a multi battle?)",
side.name
)));
}
}
_ => (),
}
for player in &side.players {
self.validate_player(side, player)?;
}
Ok(())
}
fn validate_player(&self, _: &SideData, _: &PlayerData) -> Result<()> {
Ok(())
}
/// Validates the battle options.
pub fn validate(&self) -> Result<()> {
self.validate_side(&self.side_1)?;
self.validate_side(&self.side_2)?;
Ok(())
}
}