At a high level, the pkmn engine updates a battle's state based on both players'
choices, returning a result which indicates whether the battle has ended and
what each players' options are. A non-terminal result can be fed into a generation's choices
method which returns all legal actions1, though the state of the engine may also be inspected
directly to determine information about the battle and which moves are possible. By
design, each generation's data structures are different, but the precise layout of the
battle information is outlined in the respective documentation.
More information about a battle can be generated via the -Dlog
flag when building the engine.
This flag enables the engine to write the wire protocol described in this document to a Log
.
Generally, this Log
should be a
FixedBufferStream
Writer
backed by a statically
allocated fixed-size array that gets reset
after each update
as the maximum number of bytes
written by a single update
call is bounded to a relatively small number of bytes per
generation, though since any Writer
implementation is allowed this Log
could instead write to
standard output (note that in Zig the standard out writer isn't buffered by default - you must use
a BufferedWriter
wrapper to achieve reasonable performance).
The engine's wire protocol essentially amounts to a stripped down binary translation of Pokémon
Showdown's simulator
protocol, with certain
redundant messages (e.g. |upkeep|
or |
) removed, and others subtly tweaked (there is no
|split|
message - the "omniscient" stream of information is always provided and other streams
must be recreated by driver code). Like the rest of the pkmn engine, the protocol uses
native-endianness. While the protocol may change slightly depending on the generation in question
(e.g. a Move
requires more than a single byte to encode after Generation I & II), any differences
are called out where applicable.
The pkmn-debug
tool exists to decode the binary battle and log data and
render HTML to power a human-readable debug UI. The tool expects the
following debug information to be provided to it by a binary with -Dloh
enabled.
Debug logs must start with a header which contains a byte to indicate whether -Dshowdown
compatibility mode was enabled, a byte indicating the
generation, and the initial battle state:
Start | End | Description |
---|---|---|
0 | 1 | Whether Pokémon Showdown compatibility is enabled |
1 | 2 | A number denoting the Pokémon generation |
2 | 4 | Two native-endian signed bytes encoding |
4 | 8 | Four native-endian signed bytes encoding |
8 | B+8 | The |
Following the header there maybe be any number of "frames", the last of which may be only partially complete:
Start | End | Description |
---|---|---|
0 | N |
0x00 or EOF if |
N | N+X |
0x00 or EOF if |
N+X+1 | N+X+B+1 | The |
N+X+B+2 | N+X+B+3 | The result of updating the battle |
N+X+B+3 | N+X+B+4 | The next choice for Player 1 |
N+X+B+4 | N+X+B+5 | The next choice for Player 2 |
It's important to note that by convention the debug logs start with the protocol logs that are
produced after first battle update (i.e. both sides |switch|
-ing in their first Pokémon) -
the initial battle state when no Pokémon are active and the initial required choices (pass from
both sides) and result aren't logged.2
The valid options returned by choices
can be one of three types: pass
, which will only ever
occurs in situations where only the other player gets to make a decision (e.g. when choosing which
Pokémon to switch to after their active Pokémon faints or uses Baton Pass etc), and move
or
switch
, which require additional data. These are comparable to the similarly named choice commands
in Pokémon Showdown's own
SIM-PROTOCOL.
Raw | Type | Data? |
---|---|---|
0x00 |
pass |
No |
0x01 |
move |
0-4 |
0x02 |
switch |
2-6 |
switch
takes a 1-based Pokémon slot number of an eligible party member (which must be greater than
1 as you can never switch in the active Pokémon) and move
takes a 1-based move slot number, though
is expected to be 0 in certain scenarios where the cartridge doesn't present an option to select a
move after signalling the intent to fight (e.g. during Wrap or Bide in Generation I)3. Determining
exactly which choice options are available is subtle and should be left to the engine - choices not
present in the array filled in by choices
are invalid and may corrupt the battle state or cause
the engine to crash.
Each battle update returns a result object that's made up of three things - a result type and a
choice type for either player. Any result other than None
means that the battle is considered to
be over and no further updates can be made and may result in crashes.
Raw | Description |
---|---|
0x00 |
None |
0x01 |
Player 1 Wins |
0x02 |
Player 2 Wins |
0x03 |
Player 1 & 2 Tie |
0x04 |
Error |
Error
can only be returned due to a desync/glitch, and since Pokémon Showdown mods its engine code
to avoid these this value can't be returned from an update in -Dshowdown
mode. However, the
libpkmn
C API will also set an update's result to Error
if -Dlog
protocol logging is enabled
and the buffer it has been provided runs out of space regardless of which mode its in.
The choice types included in the result match those described earlier, though they're
more akin to what Pokémon Showdown calls a sides requestType
which determines which choice
request
the side gets. In the pkmn engine the choice type from a result for a given side should similarly be
provided to the choices
function to determine which choice options exist for the side.
With -Dlog
enabled, messages are written to the Log
provided. The first byte of
each message is an integer representing the ArgType
of the message, followed by 0 or more bytes
containing the payload of the message. Game objects such as moves, species, abilities, items, types,
etc are written as their internal identifier which usually matches their public facing number, but
in cases where these differ the ids.json
can be used to decode them. A
protocol.json
containing a human-readable lookup for the ArgType
and various "reason" enums (see below) is generated from the library code and can be used
for similar purposes.
Because each protocol message is a fixed length, parsing can be terminated when a 0x00
byte is
read when the leading ArgType
header byte of a message is expected. Note that a 0x00
byte may
also appear internally within the payload of a message - only the 0x00
in the header of a message
indicates the end. This 0x00
byte will be written even in places where the previous ArgType
alone would be sufficient to indicate the end of parsing (e.g. after reading a |win|
or |turn|
message). Unlike the Pokémon Showdown simulator protocol, the pkmn engine's protocol doesn't
produce a |request|
message - as outlined earlier a driver should inspect the raw bytes of the
Battle
object in addition to the Result
to determine enough about state to determine which
choices are possible (taking into account privileged information depending on the side in question).
Several protocol messages have a "reason" field which provides further information/context about the
message and may indicate that the payload contains additional bytes. Bytes that are only present
when the reason field is a specific value are indicated by a trailing ?
. Messages which contain a
reason field will document each possible value the field can take and whether or not the specific
reason will cause the message to contain additional data. Reason fields are required to be able to
encode the information Pokémon Showdown stores in its "keyword args" (kwArgs
) mapping (e.g.
[from]
or [of]
).
Many protocol message types encode the source/target/actor as a PokemonIdent
(or Pokémon ID). From
Pokémon Showdown's documentation:
A Pokémon ID is in the form
POSITION: NAME
.
POSITION
is the spot that the Pokémon is in: it consists of thePLAYER
of the player (see|player|
), followed by a position letter (a
in singles).NAME
is the nickname of the Pokémon (or the species name, if no nickname is given).For example:
p1a: Sparky
could be a Charizard named Sparky.p1: Dragonite
could be an inactive Dragonite being healed by Heal Bell.For most commands, you can just use the position information in the Pokémon ID to identify the Pokémon. Only a few commands actually change the Pokémon in that position (
|switch|
switching,|replace|
illusion dropping,|drag|
phazing, and|detailschange|
permanent forme changes), and these all specifyDETAILS
for you to perform updates with.
Identity works a little differently in the pkmn engine, given that nicknames aren't part of the engine. While the given player, position letter and identity of the Pokémon in question are all still encoded, the identity takes the form of a single bit-packed byte:
- the most significant 3 bits are always
0
- the fourth most significant bit is
0
if the position isa
and1
if the position isb
(only relevant in doubles battles) - the fifth most significant bit is
0
for player 1 and1
for player 2. - the lowest 3 bits represent the slot (
1
through6
inclusive) of the Pokémon's original location within the party (i.e. the order a player's team was initially in when the battle started, before any Pokémon were switched).
A Pokémon Showdown-compatible PokemonIdent
can be translated from the pkmn engine identity
by driver code provided there exists a mapping from the party's original positions and Pokémon
nicknames.
Unfortunately, Pokémon Showdown's protocol doesn't allow for translating a single message at a time
in isolation. The |move|
message can be modified by a message with the LastStill
(0x01
) or
LastMiss
(0x02
) ArgType
. In the Pokémon Showdown simulator the attrLastMove
method is used
to modify a batched |move|
message before it's written with other messages as a single chunk, but
the pkmn engine streams out writes immediately and doesn't perform batching, meaning code parsing
the engine's protocol is required to do the batching instead.
When interpreting a buffer written by the pkmn Log
: if a LastStill
(0x01
) byte is encountered
and if there is a previous |move|
message in the same buffer that occurred earlier, append a
[still]
keyword arg to it. Similarly, if a LastMiss
(0x02
) byte is encountered, append a
[miss]
to the last seen |move|
message if present.
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x03 | Source | Move | Target |
+---------------+---------------+---------------+---------------+
4| Reason | [from]? |
+---------------+---------------+
Source
is the PokemonIdent
of the Pokémon that used the Move
on the
PokemonIdent
Target
for Reason
. If Reason
is 0x02
then the next byte
will indicate which Move
the |move|
is [from]
. This message may be modified later on by a
LastStill
or LastMiss
message in the same buffer (preceding).
Reason
Raw | Description | [from] ? |
---|---|---|
0x00 |
None | No |
0x01 |
|[from] |
Yes |
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x04 | Ident | Species | Level |
+---------------+---------------+---------------+---------------+
4| Current HP | Max HP |
+---------------+---------------+---------------+---------------+
8| Status |
+---------------+
The Pokémon identified by Ident
has switched in and is a level Level
Species
with Current HP
, Max HP
and Status
.
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x04 | Ident | Species | Gender |
+---------------+---------------+---------------+---------------+
4| Level | Current HP | Max HP -> |
+---------------+---------------+---------------+---------------+
8| <- Max HP | Status | Reason |
+---------------+---------------+---------------+
The Pokémon identified by Ident
has switched in due to Reason
and is a Gender
level Level
Species
with Current HP
, Max HP
and Status
.
Reason
Raw | Description |
---|---|
0x00 |
None |
0x01 |
|[from] Baton Pass |
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x05 | Ident | Reason | Move? |
+---------------+---------------+---------------+---------------+
The Pokémon identified by Ident
couldn't perform an action due to Reason
. If
the reason is 0x05
then the following byte indicates which Move
the Pokémon was unable to
perform.
Reason
Raw | Description | Move? |
---|---|---|
0x00 |
slp |
No |
0x01 |
frz |
No |
0x02 |
par |
No |
0x03 |
partiallytrapped |
No |
0x04 |
flinch |
No |
0x05 |
Disable |
Yes |
0x06 |
recharge |
No |
0x07 |
nopp |
No |
Byte/ 0 | 1 |
/ | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+
0| 0x06 | Ident |
+---------------+---------------+
The Pokémon identified by Ident
has fainted.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x07 | Turn |
+---------------+---------------+---------------+
It's now turn Turn
.
Byte/ 0 | 1 |
/ | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+
0| 0x06 | Player |
+---------------+---------------+
The Player
has won the battle.
Byte/ 0 |
/ |
|0 1 2 3 4 5 6 7|
+---------------+
0| 0x09 |
+---------------+
The battle has ended in a tie.
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x0A | Ident | Current HP |
+---------------+---------------+---------------+---------------+
4| Max HP | Status | Reason |
+---------------+---------------+---------------+---------------+
8| [of]? |
+---------------+
The Pokémon identified by Ident
has taken damage and now has the Current HP
,
Max HP
and Status
. If Reason
is in 0x05
then the following byte indicates the
PokemonIdent
is the source [of]
the damage.
Reason
Raw | Description | [of] ? |
---|---|---|
0x00 |
None | No |
0x01 |
psn |
No |
0x02 |
brn |
No |
0x03 |
confusion |
No |
0x04 |
Leech Seed |
No |
0x05 |
Recoil|[of] |
Yes |
0x06 |
[from] Spikes |
No |
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x0B | Ident | Current HP |
+---------------+---------------+---------------+---------------+
4| Max HP | Status | Reason |
+---------------+---------------+---------------+---------------+
8| [of]? |
+---------------+
Equivalent to |-damage|
(preceding), but the Pokémon has healed damage instead. If Reason
is
0x02
then the damage was healed [from]
a draining move indicated by the subsequent byte [of]
.
Reason
Raw | Description | [of] ? |
---|---|---|
0x00 |
None | No |
0x01 |
|[silent] |
No |
0x02 |
|[from] drain|[of] |
Yes |
0x03 |
|[from] Leftovers |
No |
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x0C | Ident | Status | Reason |
+---------------+---------------+---------------+---------------+
4| [from]? |
+---------------+
The Pokémon identified by Ident
has been inflicted with Status
. If Reason
is
0x01
then the next byte indicates which Move
the Status
is [from]
.
Reason
Raw | Description | [from] ? |
---|---|---|
0x00 |
None | No |
0x01 |
|[silent] |
No |
0x02 |
|[from] |
Yes |
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x0D | Ident | Status | Reason |
+---------------+---------------+---------------+---------------+
The Pokémon identified by Ident
has recovered from Status
.
Reason
Raw | Description |
---|---|
0x00 |
|[msg] |
0x01 |
|[silent] |
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x0E | Ident | Reason | Num |
+---------------+---------------+---------------+---------------+
The Pokémon identified by Ident
has been (un)boosted by Num
- 6 in a stat
indicated by the Reason
.
Reason
Raw | Description |
---|---|
0x00 |
atk|[from] Rage |
0x01 |
atk |
0x02 |
def |
0x03 |
spe |
0x04 |
spa |
0x05 |
spd |
0x06 |
accuracy |
0x07 |
evasion |
Byte/ 0 |
/ |
|0 1 2 3 4 5 6 7|
+---------------+
0| 0x0F |
+---------------+
Clears all boosts from all Pokémon on both sides.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x10 | Ident | Reason |
+---------------+---------------+---------------+
An action denoted by Reason
used by the Pokémon identified by Ident
has failed
due to its own mechanics.
Reason
Raw | Description |
---|---|
0x00 |
None |
0x01 |
slp |
0x02 |
psn |
0x03 |
brn |
0x04 |
frz |
0x05 |
par |
0x06 |
to |
0x07 |
move: Substitute |
0x08 |
move: Substitute|[weak] |
Byte/ 0 | 1 |
/ | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+
0| 0x11 | Ident |
+---------------+---------------+
A move used by the Pokémon identified by Ident
missed.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x12 | Ident | Num |
+---------------+---------------+---------------+
A multi-hit move hit the Pokémon identified by Ident
Num
times.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x13 | Ident | Move |
+---------------+---------------+---------------+
The Pokémon identified by Ident
is preparing to charge Move
.
Byte/ 0 | 1 |
/ | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+
0| 0x14 | Ident |
+---------------+---------------+
The Pokémon identified by Ident
must spend the turn recharging from a previous
move.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x15 | Ident | Reason |
+---------------+---------------+---------------+
A miscellaneous effect indicated by Reason
has activated on the Pokémon identified by
Ident
.
Reason
Raw | Description |
---|---|
0x00 |
Bide |
0x01 |
confusion |
0x02 |
move: Haze |
0x03 |
move: Mist * |
0x04 |
move: Struggle |
0x05 |
Substitute|[damage] |
0x06 |
||move: Splash |
*Note that Mist gets "upgraded" to a |-block|
message by Pokémon Showdown.
Byte/ 0 |
/ |
|0 1 2 3 4 5 6 7|
+---------------+
0| 0x16 |
+---------------+
A field condition has activated.
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x17 | Ident | Reason | Move/Types? |
+---------------+---------------+---------------+---------------+
4| [of]? |
+---------------+
A volatile status from Reason
has been inflicted on the Pokémon identified by
Ident
. If Reason
is 0x09
then the following bytes indicates which Types
the
Pokémon has changed to and the target PokemonIdent
[of]
. If Reason
is 0x0A
or 0x0B
then the following byte indicates the Move
which has been Disable-d/Mimic-ked.
Reason
Raw | Description | Move/Types? | [of] ? |
---|---|---|---|
0x00 |
Bide * |
No | No |
0x01 |
confusion |
No | No |
0x02 |
confusion|[silent] |
No | No |
0x03 |
move: Focus Energy |
No | No |
0x04 |
move: Leech Seed |
No | No |
0x05 |
Light Screen |
No | No |
0x06 |
Mist |
No | No |
0x07 |
Reflect |
No | No |
0x08 |
Substitute |
No | No |
0x09 |
typechange|...|[from] move: Conversion|[of] |
Yes | Yes |
0x0A |
Disable| |
Yes | No |
0x0B |
Mimic| |
Yes | No |
*0x00
corresponds to move: Bide
in Generation II.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x18 | Ident | Reason |
+---------------+---------------+---------------+
A volatile status from Reason
inflicted on the Pokémon identified by Ident
has
ended.
FIXME leechseed From Of
Reason
Raw | Description |
---|---|
0x00 |
Disable * |
0x01 |
confusion |
0x02 |
Bide * |
0x03 |
Substitute |
0x04 |
Disable|[silent] |
0x05 |
confusion|[silent] |
0x06 |
mist|[silent] |
0x07 |
focusenergy|[silent] |
0x08 |
leechseed|[silent] |
0x09 |
Toxic counter|[silent] |
0x0A |
lightscreen|[silent] |
0x0B |
reflect|[silent] |
0x0C |
move: Bide|[silent] |
*0x00
corresponds to move: Disable
and 0x02
to move: Bide
in Generation II.
Byte/ 0 |
/ |
|0 1 2 3 4 5 6 7|
+---------------+
0| 0x19 |
+---------------+
A OHKO move was used successfully.
Byte/ 0 | 1 |
/ | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+
0| 0x1A | Ident |
+---------------+---------------+
A move has dealt a critical hit against the Pokémon identified by Ident
.
Byte/ 0 | 1 |
/ | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+
0| 0x1B | Ident |
+---------------+---------------+
A move was super-effective against the Pokémon identified by Ident
.
Byte/ 0 | 1 |
/ | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+
0| 0x1C | Ident |
+---------------+---------------+
A move wasn't very effective against the Pokémon identified by Ident
.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x1D | Ident | Reason |
+---------------+---------------+---------------+
The Pokémon identified by Ident
is immune to a move.
Reason
Raw | Description |
---|---|
0x00 |
None |
0x01 |
|[ohko] |
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x1E | Source | Target |
+---------------+---------------+---------------+
Source
is the PokemonIdent
of the Pokémon that transformed into the Pokémon
identified by the Target
PokemonIdent
.
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x1F | Ident | Species | Gender |
+---------------+---------------+---------------+---------------+
4| Level | Current HP | Max HP -> |
+---------------+---------------+---------------+---------------+
8| <- Max HP | Status |
+---------------+---------------+
The Pokémon identified by Ident
has been dragged in and is a Gender
level
Level
Species
with Current HP
, Max HP
and Status
.
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x20 | Target | Item | Source |
+---------------+---------------+---------------+---------------+
Target
is the PokemonIdent
of the Pokémon that has had their Item
changed or
revealed [from] move: Thief
[of]
the Source
PokemonIdent
.
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x21 | Target | Item | Reason |
+---------------+---------------+---------------+---------------+
The Item
held by the Pokémon identified by Ident
has been lost or destroyed
due to Reason
.
Reason
Raw | Description |
---|---|
0x00 |
None |
0x01 |
|[eat] |
Byte/ 0 | 1 |
/ | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+
0| 0x22 | Ident |
+---------------+---------------+
The Pokémon identified by Ident
cured its team of status effects [from] move: Heal Bell
.
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x23 | Ident | Current HP |
+---------------+---------------+---------------+---------------+
4| Max HP | Status | Reason |
+---------------+---------------+---------------+---------------+
The Pokémon identified by Ident
had their HP changed to Current HP
, Max HP
and Status
.
Reason
Raw | Description |
---|---|
0x00 |
[from] move: Pain Split |
0x01 |
[from] move: Pain Split|[silent] |
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x24 | Ident | Num |
+---------------+---------------+---------------+
The Pokémon identified by Ident
has been boosted to (as opposed to by)
Num
- 6 in Attack [from] move: Belly Drum
.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x25 | Source | Target |
+---------------+---------------+---------------+
Source
is the PokemonIdent
of the Pokémon that copies the boosts of the Pokémon
identified by the Target
PokemonIdent
.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x26 | Player | Reason |
+---------------+---------------+---------------+
A side condition from Reason
started on the side belonging to Player
.
Reason
Raw | Description |
---|---|
0x00 |
Safeguard |
0x01 |
move: Light Screen |
0x02 |
Reflect |
0x03 |
Spikes |
Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| 0x27 | Player | Reason | [of]? |
+---------------+---------------+---------------+---------------+
A side condition from Reason
ended on the side belonging to Player
. Reason
is the same as
|-sidestart|
, but if Reason
is 0x03
then the following byte indicates the
PokemonIdent
is the source [of]
the spikes being removed [from] move: Rapid Spin
.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x28 | Ident | Move |
+---------------+---------------+---------------+
The Pokémon identified by Ident
used Move
which caused a temporary effect
lasting the duration of the move.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x29 | Ident | Move |
+---------------+---------------+---------------+
The Pokémon identified by Ident
used Move
which caused a temporary effect
lasting the duration of the turn.
Byte/ 0 | 1 | 2 |
/ | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+
0| 0x2A | Weather | Reason |
+---------------+---------------+---------------+
Weather
is currently in effect on the field, possibly active previously an still in effect if
Reason
is 0x01
.
Weather
Raw | Description |
---|---|
0x00 |
None |
0x01 |
Rain |
0x02 |
Sun |
0x03 |
Sandstorm |
Reason
Raw | Description |
---|---|
0x00 |
None |
0x01 |
|[upkeep] |
As mentioned earlier, any Writer
can be used to back the protocol Log
, and as such something like
an ArrayList.Writer
can be used to
support arbitrary amounts of data being written to the log each update. For performance reasons it
is desirable to be able to pre-allocate a fixed size buffer for this, where the recommended size is
determined by pkmn.LOGS_SIZE
which is guaranteed to be able to handle at least pkmn.MAX_LOGS
bytes (these constants are defined to be the maximum of all generations - each generation also has
its own parallel constants that can be used instead, e.g. pkmn.gen1.LOGS_SIZE
).
Determining the maximum amount of bytes in a single update (i.e. bytes logged by a call to update
with each player's Choice
) per generation is non-trivial and could vary greatly depending on the
constraints:
- Standard: The most restrictive constraint supported is one which considers Pokémon Showdown's "Standard" restrictions for competitive formats - Species Clause, Cleric Clause, movepool legality and bans, etc.
- Cartridge: Cartridge legality still enforces anything that can be legitimately obtained on the cartridge and used in link battles, but doesn't enforce any of Pokémon Showdown's clauses or mods.
- "Hackmons": A step further than cartridge legality, removing restrictions on moves / items / abilities / types / stats / etc - anything which can be hacked into the game before the battle.
- Fuzzing: Used for testing, the same "hackmons" constraints but also allowing for arbitrary in-battle manipulation as well to be able to set impossible combinations of volatiles statuses or side conditions etc.
Furthermore, a lax vs. strict interpretation of the RNG can be applied - whether or not any conceivable sequence of events should be considered or only those which can be obtained in practice via the actual RNG (on either the cartridge or Pokémon Showdown).
In general, the constants are be defined such that they're guaranteed to be sufficient under cartridge constraints with a lax consideration of the RNG (though taking a strict interpretation of the RNG is required in some cases to be able to conservatively set the upper bounds in specific scenarios). In practice, the constants will usually be defined to handle "hackmons" legality as that is what's useful for the engine's fuzz testing - see the precise definitions below for exact details. In most cases these scenarios have been derived with the Z3 theorem prover.
The following maximum log size scenarios were developed with help from @gigalh128.
The MAX_LOGS
constant in Generation I is determined to be 180. Achieving this requires two
burned Aerodactyl with Leech Seed, Confuse Ray, and Metronome where one is slower than the other. On
the first two turns of the battle the Aerodactyl both use Leech Seed followed by Confuse Ray, and
then in the turn where the maximum single update output is to be reached, the faster Aerodactyl uses
Metronome to proc a critical hit Fury Swipes that hits 5 times and the slower Aerodactyl uses
Metronome to proc Mirror Move which then also procs a critical hit Fury Swipes that hits 5 times.
The initial battle seed required to achieve this is output is
Details
In order to maximize log message size in Generation I several observations need to be made:
-
|move
,|switch|
,|-damage|
, and|-heal|
take up the most space in the log -
|switch|
will result in less bytes than|move|
because it won't result in any additional messages whereas|move|
can trigger many others, including|-damage|
or|-heal|
-
|move|
should|-crit|
and either be|-supereffective|
or|-resisted|
to use up more bytes - before a
|move|
a Pokémon can activate confusion, and after residual damage from a poison or burn status, Leech Seed can be triggered - neither Pokémon can
|faint|
at the end. While initially it might seem like having both|faint|
and causing one side to|win|
would be optimal (two|faint|
messages and one|win|
is 6 bytes total, a single|faint|
and a|turn|
message is 5 bytes), if a side faints you miss out on a round of damage and healing from Leech Seed which is 16 bytes - Substitute activating actually reduces the size of the log as
|-activate|
replaces the larger|-damage|
messages, and in Generation I if a substitute breaks it nullifies the rest of the move's effects - ultimately a
MultiHit
move is optimal as it can generate 5|-damage|
messages and a|-hitcount|
The most important observation is that Metronome and Mirror Move can be used in tandem to rack up
arbitrary increases in log size via mutual recursion. Metronome and Mirror Move handlers both
contain checks to prevent infinite self-recursion, but don't check for each other, meaning a
Pokémon can use Metronome to proc Mirror Move to copy their opponent's Metronome to proc Mirror Move
again etc. However, since Mirror Move works based on the last_used_move
field and Metronome using
a new move overwrites this field, Mirror Move copying an opponent's original Metronome will only
work if a charging move is triggered by the initial Metronome, meaning the other player can not also
perform this loop (and can actually not use a move on their turn at all, as they would then be
locked in by the previous turn's charging move used via Metronome).
With true randomness it seems at face value that with the proper set up the chances of achieving
Metronome → Mirror Move calls would be
In Pokémon Showdown there are several frame advances between each call (e.g. a consequential roll to
hit, an advance to re-target, etc) and setting up the RNG to accomplish arbitrary recursion isn't
feasible in practice. However, in Pokémon Red there is only an inconsequential (i.e. the value is
ignored) critical hit roll for both Metronome and Mirror Move between each roll to determine which
move to use and the seed can be used to set up 9 arbitrary values. Define
Thus starting with a seed of
There are then two hypothetical scenarios that must be considered to determine the upper bound on the maximum log size of a single update - one where a single player gets to benefit from the Metronome → Mirror Move → ... → Metronome → multi-hit move and the other side is locked into a charging move or one where both players simply call a multi-hit move through a single iteration of Metronome → Mirror Move → multi-hit.
-
Scenario 1: recursion + charge move (188 bytes)
-
|-activate|
confusion: 2×3 bytes -
|move|
Metronome →|move|
Mirror Move P1 recursion: 1×5 + 9×6 bytes -
|move|
multi-hit: 6 bytes -
|move|
P2 turn 2 charging move: 6 bytes -
|-crit|
: 2×2 bytes -
|supereffective|
or|resisted|
: 2×2 bytes -
|-damage|
multi-hit: 5×8 bytes -
|-hitcount|
: 3 bytes -
|-damage|
charging: 8 bytes -
|-damage|
poison or burn: 2×8 bytes -
|-damage|
Leech Seed: 2×8 bytes -
|-heal|
Leech Seed: 2×8 bytes -
|turn|
: 3 bytes -
0x00
: 1 byte (end of buffer)
-
-
Scenario 2: both multi-hit (186 bytes)
-
|-activate|
confusion: 2×3 bytes -
|move|
Metronome →|move|
Mirror Move ->|move|
multi-hit: 2×5 + 4×6 bytes -
|-crit|
: 2×2 bytes -
|supereffective|
or|resisted|
: 2×2 bytes -
|-damage|
multi-hit: 10×8 bytes -
|-hitcount|
: 2×3 bytes -
|-damage|
poison or burn: 2×8 bytes -
|-damage|
Leech Seed: 2×8 bytes -
|-heal|
Leech Seed: 2×8 bytes -
|turn|
: 3 bytes -
0x00
: 1 byte (end of buffer)
-
Z3 can be used to test out these scenarios - it can be shown that despite 9 levels of recursive Metronome → Mirror Move being possible in a vacuum from the initial seed, there is no way to achieve the first scenario after burning through the rolls on the first two turns of setup to be able to still proc specific Metronome rolls after. For the second scenario the problem is that there needs to be an extra turn of setup for Player 1 to be able to Mirror Move a multi-hit move which also takes too many rolls after the initial seed to be able to set up the rest of the scenario. However, by slightly compromising and not requiring Player 1 to proc Mirror Move only lose 6 bytes and thus arrive at 180 bytes for the maximum log size for Generation I.
#!/usr/bin/env python
from z3 import *
for name, move, hit in [
('Spike Cannon', 131, 255),
('Double Slap', 3, 215),
('Comet Punch', 4, 215),
('Fury Attack', 31, 215),
('Pin Missile', 42, 215),
('Barrage', 140, 215),
('Fury Swipes', 154, 203),
]:
N = 5
for d1 in range(N):
for d2 in range(N):
for m1 in range(N):
for m2 in range(N):
total = 9 + 15 + d1 + d2 + m1 + m2
state = [BitVec('state%s' % (i + 1), 8) for i in range(total)]
s = Solver()
for i in range(total - 9):
s.add(state[i + 9] == state[i] * 5 + 1)
# NOTE: first 9 states must all be < 253
s.assert_and_track(ULE(state[0] * 5 + 1, 228), 'Turn 1: P1 Leech Seed hit')
s.assert_and_track(ULE(state[1] * 5 + 1, 228), 'Turn 1: P2 Leech Seed hit')
s.assert_and_track(ULT(state[2] * 5 + 1, 253), 'Turn 2: P1 Confuse Ray hit')
s.assert_and_track(And(ULT(state[3] * 5 + 1, 253), UGE(((state[3] * 5 + 1) & 3) + 2, 3)),
'Turn 2: P2 confusion duration (any)')
s.assert_and_track(ULT(state[4] * 5 + 1, 128), 'Turn 2: P2 avoid confusion self-hit')
s.assert_and_track(ULT(state[5] * 5 + 1, 253), 'Turn 2: P2 Confuse Ray hit')
s.assert_and_track(ULT(state[6] * 5 + 1, 253), 'Turn 2: P1 confusion duration (any')
s.assert_and_track(ULT(state[7] * 5 + 1, 128), 'Turn 3: P1 avoid confusion self-hit')
s.assert_and_track(ULT(state[8] * 5 + 1, 253), 'Turn 3: P1 Metronome crit (any)')
i = 9
for m in range(m1):
s.assert_and_track(UGE(state[i] * 5 + 1, 163), f'Turn 3: P1 Metronome no-op {m}')
i += 1
s.assert_and_track(state[i] * 5 + 1 == move, f'Turn 3: P1 Metronome proc {name}')
i += 1
s.assert_and_track(ULT(RotateLeft(state[i] * 5 + 1, 3), 65), f'Turn 3: P1 {name} crits')
i += 1
for d in range(d1):
s.assert_and_track(ULT(RotateRight(state[i] * 5 + 1, 1), 217),
f'Turn 3: P1 {name} damage roll no-op {d}')
i += 1
s.assert_and_track(UGE(RotateRight(state[i] * 5 + 1, 1), 217),
f'Turn 3: P1 {name} damage roll')
i += 1
s.assert_and_track(ULE(state[i] * 5 + 1, hit), f'Turn 3: P1 {name} hit')
i += 1
s.assert_and_track(UGE((state[i] * 5 + 1) & 3, 2), f'Turn 3: P1 {name} first hitcount')
i += 1
s.assert_and_track(((state[i] * 5 + 1) & 3) + 2 == 5, f'Turn 3: P1 {name} max hitcount')
i += 1
s.assert_and_track(ULT(state[i] * 5 + 1, 128), 'Turn 3: P2 avoid confusion self-hit')
i += 1
s.assert_and_track(ULT(state[i] * 5 + 1, 255), 'Turn 3: P2 Metronome crit (any)')
i += 1
for m in range(m2):
s.assert_and_track(UGE(state[i] * 5 + 1, 163), f'Turn 3: P2 Metronome no-op {m}')
i += 1
s.assert_and_track(state[i] * 5 + 1 == 119, 'Turn 3: P2 Metronome proc MirrorMove')
i += 1
s.assert_and_track(ULT(state[i] * 5 + 1, 255), 'Turn 3: P2 MirrorMove crit (any)')
i += 1
s.assert_and_track(ULT(RotateLeft(state[i] * 5 + 1, 3), 65), f'Turn 3: P2 {name} crits')
i += 1
for d in range(d2):
s.assert_and_track(ULT(RotateRight(state[i] * 5 + 1, 1), 217),
f'Turn 3: P2 {name} damage roll no-op {d}')
i += 1
s.assert_and_track(UGE(RotateRight(state[i] * 5 + 1, 1), 217),
f'Turn 3: P2 {name} damage roll')
i += 1
s.assert_and_track(ULE(state[i] * 5 + 1, hit), f'Turn 3: P2 {name} hit')
i += 1
s.assert_and_track(UGE((state[i] * 5 + 1) & 3, 2), f'Turn 3: P2 {name} first hitcount')
i += 1
s.assert_and_track(((state[i] * 5 + 1) & 3) + 2 == 5, f'Turn 3: P2 {name} max hitcount')
print(name, end = ': ')
if (s.check() != unsat):
m = s.model()
for i in range(8):
print(m[state[i]], end = ', ')
print(m[state[8]])
exit(0)
else:
print(s.unsat_core())
exit(1)
TODO
Footnotes
-
The
choices
method leaks information in certain cases where legal decisions would not be known to a user until after having already attempted a choice (e.g. that a Pokémon has been trapped or has had a move that has been blocked due to an opponent's use of Imprison). This is a non-issue for the use case of games being played out randomly via a machine, but a simulator for human players built on top of the pkmn engine would need to provide an alternative implementation ofchoices
. ↩ -
In generations with Team Preview it's likely that the convention around the initial data frame will change and that a
0x00
dummy byte will be used for the pre-battle log, though given that the engine currently doesn't support these later generations such changes are speculative. ↩ -
The data value for a move choice
move
must be in the range of 1-4 when in Pokémon Showdown compatibility mode as its choice selection behavior is different (i.e. incorrect). ↩