mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-08-28 12:04:28 -05:00
Battle toJSON/fromJSON (#5427)
This commit is contained in:
committed by
Guangcong Luo
parent
1669fde631
commit
df3d9b91ef
@@ -269,7 +269,7 @@ let Formats = [
|
||||
canUseRandomTeam: true,
|
||||
debug: true,
|
||||
maxLevel: 9999,
|
||||
onBegin() { this.trunc = n => Math.trunc(n); },
|
||||
trunc(n) { return Math.trunc(n); },
|
||||
defaultLevel: 100,
|
||||
teamLength: {
|
||||
validate: [1, 24],
|
||||
@@ -552,7 +552,7 @@ let Formats = [
|
||||
searchShow: false,
|
||||
canUseRandomTeam: true,
|
||||
maxLevel: 9999,
|
||||
onBegin() { this.trunc = n => Math.trunc(n); },
|
||||
trunc(n) { return Math.trunc(n); },
|
||||
defaultLevel: 100,
|
||||
debug: true,
|
||||
teamLength: {
|
||||
@@ -1435,7 +1435,7 @@ let Formats = [
|
||||
canUseRandomTeam: true,
|
||||
debug: true,
|
||||
maxLevel: 9999,
|
||||
onBegin() { this.trunc = n => Math.trunc(n); },
|
||||
trunc(n) { return Math.trunc(n); },
|
||||
defaultLevel: 100,
|
||||
// no restrictions, for serious (other than team preview)
|
||||
ruleset: ['Team Preview', 'Cancel Mod'],
|
||||
@@ -1517,7 +1517,7 @@ let Formats = [
|
||||
searchShow: false,
|
||||
canUseRandomTeam: true,
|
||||
maxLevel: 9999,
|
||||
onBegin() { this.trunc = n => Math.trunc(n); },
|
||||
trunc(n) { return Math.trunc(n); },
|
||||
defaultLevel: 100,
|
||||
debug: true,
|
||||
// no restrictions, for serious (other than team preview)
|
||||
@@ -1548,7 +1548,7 @@ let Formats = [
|
||||
searchShow: false,
|
||||
canUseRandomTeam: true,
|
||||
maxLevel: 9999,
|
||||
onBegin() { this.trunc = n => Math.trunc(n); },
|
||||
trunc(n) { return Math.trunc(n); },
|
||||
defaultLevel: 100,
|
||||
debug: true,
|
||||
// no restrictions, for serious (other than team preview)
|
||||
@@ -1642,7 +1642,7 @@ let Formats = [
|
||||
canUseRandomTeam: true,
|
||||
debug: true,
|
||||
maxLevel: 9999,
|
||||
onBegin() { this.trunc = n => Math.trunc(n); },
|
||||
trunc(n) { return Math.trunc(n); },
|
||||
defaultLevel: 100,
|
||||
// no restrictions, for serious (other than team preview)
|
||||
ruleset: ['Team Preview', 'Cancel Mod'],
|
||||
@@ -1691,7 +1691,7 @@ let Formats = [
|
||||
canUseRandomTeam: true,
|
||||
debug: true,
|
||||
maxLevel: 9999,
|
||||
onBegin() { this.trunc = n => Math.trunc(n); },
|
||||
trunc(n) { return Math.trunc(n); },
|
||||
defaultLevel: 100,
|
||||
// no restrictions, for serious (other than team preview)
|
||||
ruleset: ['Team Preview', 'Cancel Mod'],
|
||||
@@ -1770,7 +1770,7 @@ let Formats = [
|
||||
canUseRandomTeam: true,
|
||||
debug: true,
|
||||
maxLevel: 9999,
|
||||
onBegin() { this.trunc = n => Math.trunc(n); },
|
||||
trunc(n) { return Math.trunc(n); },
|
||||
defaultLevel: 100,
|
||||
// no restrictions
|
||||
ruleset: ['Cancel Mod'],
|
||||
@@ -1803,7 +1803,7 @@ let Formats = [
|
||||
canUseRandomTeam: true,
|
||||
debug: true,
|
||||
maxLevel: 9999,
|
||||
onBegin() { this.trunc = n => Math.trunc(n); },
|
||||
trunc(n) { return Math.trunc(n); },
|
||||
defaultLevel: 100,
|
||||
// no restrictions
|
||||
ruleset: ['Cancel Mod'],
|
||||
|
||||
@@ -2132,7 +2132,7 @@ let BattleAbilities = {
|
||||
|
||||
// only reset .showCure if it's false
|
||||
// (once you know a Pokemon has Natural Cure, its cures are always known)
|
||||
if (!pokemon.showCure) delete pokemon.showCure;
|
||||
if (!pokemon.showCure) pokemon.showCure = undefined;
|
||||
},
|
||||
id: "naturalcure",
|
||||
name: "Natural Cure",
|
||||
|
||||
@@ -92,7 +92,7 @@ let BattleScripts = {
|
||||
runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect) {
|
||||
let target = this.getTarget(pokemon, moveOrMoveName, targetLoc);
|
||||
let move = this.getActiveMove(moveOrMoveName);
|
||||
if (target && target.subFainted) delete target.subFainted;
|
||||
if (target && target.subFainted) target.subFainted = null;
|
||||
|
||||
this.setActiveMove(move, pokemon, target);
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ let BattleScripts = {
|
||||
runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect) {
|
||||
let move = this.getActiveMove(moveOrMoveName);
|
||||
let target = this.getTarget(pokemon, move, targetLoc);
|
||||
if (target && target.subFainted) delete target.subFainted;
|
||||
if (target && target.subFainted) target.subFainted = null;
|
||||
|
||||
this.setActiveMove(move, pokemon, target);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {Field} from './field';
|
||||
import {Pokemon} from './pokemon';
|
||||
import {PRNG, PRNGSeed} from './prng';
|
||||
import {Side} from './side';
|
||||
import {State} from './state';
|
||||
|
||||
/** A Pokemon that has fainted. */
|
||||
interface FaintedPokemon {
|
||||
@@ -30,6 +31,7 @@ interface BattleOptions {
|
||||
p3?: PlayerOptions; // Player 3 data
|
||||
p4?: PlayerOptions; // Player 4 data
|
||||
debug?: boolean; // show debug mode option
|
||||
deserialized?: boolean;
|
||||
strictChoices?: boolean; // whether invalid choices should throw
|
||||
}
|
||||
|
||||
@@ -48,6 +50,7 @@ export type RequestState = 'teampreview' | 'move' | 'switch' | '';
|
||||
export class Battle extends Dex.ModdedDex {
|
||||
readonly id: '';
|
||||
readonly debugMode: boolean;
|
||||
readonly deserialized: boolean;
|
||||
readonly strictChoices: boolean;
|
||||
readonly format: string;
|
||||
readonly formatData: AnyObject;
|
||||
@@ -118,6 +121,7 @@ export class Battle extends Dex.ModdedDex {
|
||||
|
||||
this.id = '';
|
||||
this.debugMode = format.debug || !!options.debug;
|
||||
this.deserialized = !!options.deserialized;
|
||||
this.strictChoices = !!options.strictChoices;
|
||||
this.format = format.id;
|
||||
this.formatData = {id: format.id};
|
||||
@@ -207,6 +211,14 @@ export class Battle extends Dex.ModdedDex {
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): AnyObject {
|
||||
return State.serializeBattle(this);
|
||||
}
|
||||
|
||||
static fromJSON(serialized: string | AnyObject): Battle {
|
||||
return State.deserializeBattle(serialized);
|
||||
}
|
||||
|
||||
get p1() {
|
||||
return this.sides[0];
|
||||
}
|
||||
@@ -1020,12 +1032,34 @@ export class Battle extends Dex.ModdedDex {
|
||||
type = this.requestState;
|
||||
}
|
||||
|
||||
// default to no request
|
||||
const requests: any[] = Array(this.sides.length).fill(null);
|
||||
for (const side of this.sides) {
|
||||
side.activeRequest = null;
|
||||
}
|
||||
|
||||
const maxTeamSize = this.getMaxTeamSize();
|
||||
if (type === 'teampreview') {
|
||||
this.add('teampreview' + (maxTeamSize !== 6 ? '|' + maxTeamSize : ''));
|
||||
}
|
||||
|
||||
const requests = this.getRequests(type, maxTeamSize);
|
||||
for (let i = 0; i < this.sides.length; i++) {
|
||||
this.sides[i].emitRequest(requests[i]);
|
||||
}
|
||||
|
||||
if (this.sides.every(side => side.isChoiceDone())) {
|
||||
throw new Error(`Choices are done immediately after a request`);
|
||||
}
|
||||
}
|
||||
|
||||
getMaxTeamSize() {
|
||||
const teamLengthData = this.getFormat().teamLength;
|
||||
return (teamLengthData && teamLengthData.battle) || 6;
|
||||
}
|
||||
|
||||
getRequests(type: RequestState, maxTeamSize: number) {
|
||||
// default to no request
|
||||
const requests: any[] = Array(this.sides.length).fill(null);
|
||||
|
||||
switch (type) {
|
||||
case 'switch': {
|
||||
for (let i = 0; i < this.sides.length; i++) {
|
||||
@@ -1042,10 +1076,6 @@ export class Battle extends Dex.ModdedDex {
|
||||
}
|
||||
|
||||
case 'teampreview':
|
||||
const teamLengthData = this.getFormat().teamLength;
|
||||
let maxTeamSize = teamLengthData && teamLengthData.battle;
|
||||
this.add('teampreview' + (maxTeamSize ? '|' + maxTeamSize : ''));
|
||||
if (!maxTeamSize) maxTeamSize = 6;
|
||||
for (let i = 0; i < this.sides.length; i++) {
|
||||
const side = this.sides[i];
|
||||
side.maxTeamSize = maxTeamSize;
|
||||
@@ -1065,19 +1095,14 @@ export class Battle extends Dex.ModdedDex {
|
||||
|
||||
const allRequestsMade = requests.every(request => request);
|
||||
for (let i = 0; i < this.sides.length; i++) {
|
||||
const side = this.sides[i];
|
||||
const request = requests[i];
|
||||
if (request) {
|
||||
if (!this.supportCancel || !allRequestsMade) request.noCancel = true;
|
||||
side.emitRequest(request);
|
||||
if (requests[i]) {
|
||||
if (!this.supportCancel || !allRequestsMade) requests[i].noCancel = true;
|
||||
} else {
|
||||
side.emitRequest({wait: true, side: side.getRequestData()});
|
||||
requests[i] = {wait: true, side: this.sides[i].getRequestData()};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sides.every(side => side.isChoiceDone())) {
|
||||
throw new Error(`Choices are done immediately after a request`);
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
|
||||
tiebreak() {
|
||||
@@ -1544,6 +1569,8 @@ export class Battle extends Dex.ModdedDex {
|
||||
}
|
||||
|
||||
start() {
|
||||
// deserialized should use restart instead
|
||||
if (this.deserialized) return;
|
||||
// need all players to start
|
||||
if (!this.sides.every(side => !!side)) return;
|
||||
|
||||
@@ -1568,9 +1595,8 @@ export class Battle extends Dex.ModdedDex {
|
||||
this.add('rated', typeof this.rated === 'string' ? this.rated : '');
|
||||
}
|
||||
|
||||
if (format.onBegin) {
|
||||
format.onBegin.call(this);
|
||||
}
|
||||
if (format.onBegin) format.onBegin.call(this);
|
||||
if (format.trunc) this.trunc = format.trunc;
|
||||
for (const rule of this.getRuleTable(format).keys()) {
|
||||
if (rule.startsWith('+') || rule.startsWith('-') || rule.startsWith('!')) continue;
|
||||
const subFormat = this.getFormat(rule);
|
||||
@@ -1590,6 +1616,16 @@ export class Battle extends Dex.ModdedDex {
|
||||
if (!this.requestState) this.go();
|
||||
}
|
||||
|
||||
restart(send?: (type: string, data: string | string[]) => void) {
|
||||
if (!this.deserialized) throw new Error('Attempt to restart a battle which has not been deserialized');
|
||||
|
||||
const format = this.getFormat();
|
||||
if (format.trunc) this.trunc = format.trunc;
|
||||
|
||||
// @ts-ignore - readonly
|
||||
this.send = send;
|
||||
}
|
||||
|
||||
boost(
|
||||
boost: SparseBoostsTable, target: Pokemon | null = null, source: Pokemon | null = null,
|
||||
effect: Effect | null = null, isSecondary: boolean = false, isSelf: boolean = false) {
|
||||
@@ -2667,7 +2703,7 @@ export class Battle extends Dex.ModdedDex {
|
||||
foeActive.removeVolatile('substitutebroken');
|
||||
}
|
||||
}
|
||||
delete action.pokemon.draggedIn;
|
||||
action.pokemon.draggedIn = null;
|
||||
break;
|
||||
case 'runPrimal':
|
||||
if (!action.pokemon.transformed) {
|
||||
@@ -3004,7 +3040,7 @@ export class Battle extends Dex.ModdedDex {
|
||||
const format = this.getFormat();
|
||||
let team = options.team;
|
||||
if (typeof team === 'string') team = Dex.fastUnpackTeam(team);
|
||||
if (!format.team && team) return team;
|
||||
if ((!format.team || this.deserialized) && team) return team;
|
||||
|
||||
if (!options.seed) {
|
||||
options.seed = PRNG.generateSeed();
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import {State} from './state';
|
||||
|
||||
export class Field {
|
||||
readonly battle: Battle;
|
||||
readonly id: '';
|
||||
|
||||
weather: string;
|
||||
weatherData: AnyObject;
|
||||
@@ -15,6 +19,7 @@ export class Field {
|
||||
|
||||
constructor(battle: Battle) {
|
||||
this.battle = battle;
|
||||
this.id = '';
|
||||
|
||||
this.weather = '';
|
||||
this.weatherData = {id: ''};
|
||||
@@ -23,6 +28,10 @@ export class Field {
|
||||
this.pseudoWeather = {};
|
||||
}
|
||||
|
||||
toJSON(): AnyObject {
|
||||
return State.serializeField(this);
|
||||
}
|
||||
|
||||
setWeather(status: string | PureEffect, source: Pokemon | 'debug' | null = null, sourceEffect: Effect | null = null) {
|
||||
status = this.battle.getEffect(status);
|
||||
if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect;
|
||||
|
||||
@@ -1054,6 +1054,7 @@ interface FormatsData extends EventMethods {
|
||||
onValidateTeam?: (this: ModdedDex, team: PokemonSet[], format: Format, teamHas: AnyObject) => string[] | void
|
||||
validateSet?: (this: Validator, set: PokemonSet, teamHas: AnyObject) => string[] | void
|
||||
validateTeam?: (this: Validator, team: PokemonSet[], removeNicknames: boolean) => string[] | void,
|
||||
trunc?: (n: number) => number;
|
||||
section?: string,
|
||||
column?: number
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* @license MIT license
|
||||
*/
|
||||
|
||||
import {State} from './state';
|
||||
|
||||
/** A Pokemon's move slot. */
|
||||
interface MoveSlot {
|
||||
id: string;
|
||||
@@ -53,7 +55,7 @@ export class Pokemon {
|
||||
status: string;
|
||||
statusData: AnyObject;
|
||||
volatiles: AnyObject;
|
||||
showCure: boolean;
|
||||
showCure?: boolean;
|
||||
|
||||
/**
|
||||
* These are the basic stats that appear on the in-game stats screen:
|
||||
@@ -384,6 +386,10 @@ export class Pokemon {
|
||||
this.m = {};
|
||||
}
|
||||
|
||||
toJSON(): AnyObject {
|
||||
return State.serializePokemon(this);
|
||||
}
|
||||
|
||||
get moves(): readonly string[] {
|
||||
return this.moveSlots.map(moveSlot => moveSlot.id);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import {RequestState} from './battle';
|
||||
import {Pokemon} from './pokemon';
|
||||
import {State} from './state';
|
||||
|
||||
/** A single action that can be chosen. */
|
||||
interface ChosenAction {
|
||||
@@ -13,6 +14,7 @@ interface ChosenAction {
|
||||
pokemon?: Pokemon; // the pokemon doing the action
|
||||
targetLoc?: number; // relative location of the target to pokemon (move action only)
|
||||
moveid: string; // a move to use (move action only)
|
||||
move?: ActiveMove; // the active move corresponding to moveid (move action only)
|
||||
target?: Pokemon; // the target of the action
|
||||
index?: number; // the chosen index in Team Preview
|
||||
side?: Side; // the action's side
|
||||
@@ -22,7 +24,7 @@ interface ChosenAction {
|
||||
}
|
||||
|
||||
/** What the player has chosen to happen. */
|
||||
interface Choice {
|
||||
export interface Choice {
|
||||
cantUndo: boolean; // true if the choice can't be cancelled because of the maybeTrapped issue
|
||||
error: string; // contains error text in the case of a choice error
|
||||
actions: ChosenAction[]; // array of chosen actions
|
||||
@@ -121,6 +123,10 @@ export class Side {
|
||||
this.lastMove = null;
|
||||
}
|
||||
|
||||
toJSON(): AnyObject {
|
||||
return State.serializeSide(this);
|
||||
}
|
||||
|
||||
get requestState(): RequestState {
|
||||
if (!this.activeRequest || this.activeRequest.wait) return '';
|
||||
if (this.activeRequest.teamPreview) return 'teampreview';
|
||||
|
||||
415
sim/state.ts
Normal file
415
sim/state.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
/**
|
||||
* Simulator State
|
||||
* Pokemon Showdown - http://pokemonshowdown.com/
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import {Battle} from './battle';
|
||||
import Dex = require('./dex');
|
||||
import * as Data from './dex-data';
|
||||
import {Field} from './field';
|
||||
import {Pokemon} from './pokemon';
|
||||
import {PRNG} from './prng';
|
||||
import {Choice, Side} from './side';
|
||||
|
||||
// The simulator supports up to 24 different Pokemon on a team. Serialization
|
||||
// uses letters instead of numbers to indicate indices/positions, but where
|
||||
// the simulator only gives a position to active Pokemon, serialization
|
||||
// uses letters for every Pokemon on a team. Active pokemon will still
|
||||
// have the same letter as their position would indicate, but non-active
|
||||
// team members are filled in with subsequent letters.
|
||||
const POSITIONS = 'abcdefghijklmnopqrstuvwx';
|
||||
|
||||
// Several types we serialize as 'references' in the form '[Type]' because
|
||||
// they are either circular or they are (or at least, should be) immutable
|
||||
// and thus can simply be reconsituted as needed.
|
||||
// NOTE: Template is not strictly immutable as some OM formats rely on an
|
||||
// onModifyTemplate event - deserialization is not possible for such formats.
|
||||
type Referable = Battle | Field | Side | Pokemon | PureEffect | Ability | Item | Move | Template;
|
||||
|
||||
// Certain fields are either redundant (transient caches, constants, duplicate
|
||||
// information) or require special treatment. These sets contain the specific
|
||||
// keys which we skip during default (de)serialization and (the keys which)
|
||||
// need special treatment from these sets are then handled manually.
|
||||
|
||||
// Battle inherits from Dex, but all of Dex's fields are redundant - we can
|
||||
// just recreate the Dex from the format.
|
||||
const BATTLE = new Set([
|
||||
...Object.keys(Dex), 'id', 'log', 'inherit', 'cachedFormat',
|
||||
'zMoveTable', 'teamGenerator', 'NOT_FAIL', 'FAIL', 'SILENT_FAIL',
|
||||
'field', 'sides', 'prng', 'hints', 'deserialized',
|
||||
]);
|
||||
const FIELD = new Set(['id', 'battle']);
|
||||
const SIDE = new Set(['battle', 'team', 'pokemon', 'choice', 'activeRequest']);
|
||||
const POKEMON = new Set([
|
||||
'side', 'battle', 'set', 'name', 'fullname', 'id', 'species',
|
||||
'speciesid', 'happiness', 'level', 'pokeball', 'baseMoveSlots',
|
||||
]);
|
||||
const CHOICE = new Set(['switchIns']);
|
||||
const ACTIVE_MOVE = new Set(['move']);
|
||||
|
||||
export const State = new class {
|
||||
// REFERABLE is used to determine which objects are of the Referable type by
|
||||
// comparing their constructors. Unfortunately, we need to set this dynamically
|
||||
// due to circular module dependencies on Battle and Field instead
|
||||
// of simply initializing it as a const. See isReferable for where this
|
||||
// gets lazily created on demand.
|
||||
// tslint:disable-next-line: ban-types
|
||||
private REFERABLE?: Set<Function>;
|
||||
|
||||
serializeBattle(battle: Battle): /* Battle */ AnyObject {
|
||||
const state: /* Battle */ AnyObject = this.serialize(battle, BATTLE, battle);
|
||||
state.field = this.serializeField(battle.field);
|
||||
state.sides = new Array(battle.sides.length);
|
||||
for (const [i, side] of battle.sides.entries()) {
|
||||
state.sides[i] = this.serializeSide(side);
|
||||
}
|
||||
state.prng = battle.prng.seed;
|
||||
state.hints = Array.from(battle.hints);
|
||||
// We treat log specially because we only set it back on Battle after everything
|
||||
// else has been deserialized to avoid anything accidentally `add`-ing to it.
|
||||
state.log = battle.log;
|
||||
return state;
|
||||
}
|
||||
|
||||
// Deserialization can only really be done on the root Battle object as
|
||||
// the leaf nodes like Side or Pokemon contain backreferences to Battle
|
||||
// but don't contain the information to fill it in because the cycles in
|
||||
// the graph have been serialized as references. Once deserialzized, the
|
||||
// Battle can then be restarted (and provided with a `send` function for
|
||||
// receiving updates).
|
||||
deserializeBattle(serialized: string | /* Battle */ AnyObject): Battle {
|
||||
const state: /* Battle */ AnyObject =
|
||||
typeof serialized === 'string' ? JSON.parse(serialized) : serialized;
|
||||
const options = {
|
||||
formatid: state.format,
|
||||
seed: state.prngSeed,
|
||||
rated: state.rated,
|
||||
debug: state.debugMode,
|
||||
// We need to tell the Battle that we're creating that it's been
|
||||
// deserialized so that it allows us to populate it correctly and
|
||||
// doesn't attempt to start playing out until we're ready.
|
||||
deserialized: true,
|
||||
strictChoices: state.strictChoices,
|
||||
};
|
||||
for (const side of state.sides) {
|
||||
// When we instantiate the Battle again we need the pokemon to be in
|
||||
// the correct order they were in at the start of the Battle which was
|
||||
// serialized. See serializeSide below for an explanation about the
|
||||
// encoding format used deserializeSide for where we reorder the Side's
|
||||
// pokemon to match their ordering at the point of serialization.
|
||||
const team = side.team.split(side.team.length > 9 ? ',' : '');
|
||||
// @ts-ignore - index signature
|
||||
options[side.id] = {
|
||||
name: side.name,
|
||||
avatar: side.avatar,
|
||||
team: team.map((p: string) => side.pokemon[Number(p) - 1].set),
|
||||
};
|
||||
}
|
||||
// We create the Battle, allowing it to instantiate the Field/Side/Pokemon
|
||||
// objects for us. The objects it creates will be incorrect, but we descend
|
||||
// down through the fields and repopulate all of the objects with the
|
||||
// correct state afterwards.
|
||||
const battle = new Battle(options);
|
||||
// Calling `new Battle(...)` means side.pokemon is ordered to match what it
|
||||
// was at the start of the battle (state.team), but we need to order the Pokemon
|
||||
// back in their correct order based on how the battle has progressed. We need
|
||||
// do to this before making any deserialization calls so that `fromRef` will
|
||||
// be correct.
|
||||
for (const [i, s] of state.sides.entries()) {
|
||||
const side = battle.sides[i];
|
||||
const ordered = new Array(side.pokemon.length);
|
||||
const team = s.team.split(s.team.length > 9 ? ',' : '');
|
||||
for (const [j, pos] of team.entries()) {
|
||||
ordered[Number(pos) - 1] = side.pokemon[j];
|
||||
}
|
||||
side.pokemon = ordered;
|
||||
}
|
||||
this.deserialize(state, battle, BATTLE, battle);
|
||||
this.deserializeField(state.field, battle.field);
|
||||
let activeRequests = false;
|
||||
for (const [i, side] of state.sides.entries()) {
|
||||
this.deserializeSide(side, battle.sides[i]);
|
||||
activeRequests = activeRequests || side.activeRequest === undefined;
|
||||
}
|
||||
// Since battle.getRequests depends on the state of each side we can't combine
|
||||
// this loop with the one above which deserializes the sides. We also only do this
|
||||
// if there are any active requests, not only to avoid have to recompute request
|
||||
// states we wouldnt be using, but also because battle.getRequests will mutate
|
||||
// state on occasion (eg. `pokemon.getMoves` sets `pokemon.trapped = true` if locked).
|
||||
if (activeRequests) {
|
||||
const requests = battle.getRequests(battle.requestState, battle.getMaxTeamSize());
|
||||
for (const [i, side] of state.sides.entries()) {
|
||||
battle.sides[i].activeRequest = side.activeRequest === null ? null : requests[i];
|
||||
}
|
||||
}
|
||||
battle.prng = new PRNG(state.prng);
|
||||
// @ts-ignore - readonly
|
||||
battle.hints = new Set(state.hints);
|
||||
// @ts-ignore - readonly
|
||||
battle.log = state.log;
|
||||
return battle;
|
||||
}
|
||||
|
||||
serializeField(field: Field): /* Field */ AnyObject {
|
||||
return this.serialize(field, FIELD, field.battle);
|
||||
}
|
||||
|
||||
private deserializeField(state: /* Field */ AnyObject, field: Field) {
|
||||
this.deserialize(state, field, FIELD, field.battle);
|
||||
}
|
||||
|
||||
serializeSide(side: Side): /* Side */ AnyObject {
|
||||
const state: /* Side */ AnyObject = this.serialize(side, SIDE, side.battle);
|
||||
state.pokemon = new Array(side.pokemon.length);
|
||||
const team = new Array(side.pokemon.length);
|
||||
for (const [i, pokemon] of side.pokemon.entries()) {
|
||||
state.pokemon[i] = this.serializePokemon(pokemon);
|
||||
team[side.team.indexOf(pokemon.set)] = i + 1;
|
||||
}
|
||||
// We encode the team such that it could be used as a valid `/team` command
|
||||
// during decoding to transform the current ordering of the serialized Side's
|
||||
// pokemon array into the original team ordering at the start of the battle.
|
||||
// This is *not* the same as the original `/team` command used to order the
|
||||
// pokemon in team preview, but this encoding results in the most intuitive
|
||||
// and readable debugging of the raw JSON, so we're willing to add a small
|
||||
// amount of complexity to the encoding/decoding process to accommodate this.
|
||||
state.team = team.join(team.length > 9 ? ',' : '');
|
||||
state.choice = this.serializeChoice(side.choice, side.battle);
|
||||
// If activeRequest is null we encode it as a tombstone indicator to ensure
|
||||
// that during serialization when we recompute the activeRequest we don't turn
|
||||
// `activeRequest = null` into `activeRequest = { wait: true, ... }`.
|
||||
if (side.activeRequest === null) state.activeRequest = null;
|
||||
return state;
|
||||
}
|
||||
|
||||
private deserializeSide(state: /* Side */ AnyObject, side: Side) {
|
||||
this.deserialize(state, side, SIDE, side.battle);
|
||||
for (const [i, pokemon] of state.pokemon.entries()) {
|
||||
this.deserializePokemon(pokemon, side.pokemon[i]);
|
||||
}
|
||||
this.deserializeChoice(state.choice, side.choice, side.battle);
|
||||
}
|
||||
|
||||
serializePokemon(pokemon: Pokemon): /* Pokemon */ AnyObject {
|
||||
const state: /* Pokemon */ AnyObject = this.serialize(pokemon, POKEMON, pokemon.battle);
|
||||
state.set = pokemon.set;
|
||||
// Only serialize the baseMoveSlots if they differ from moveSlots. We could get fancy and
|
||||
// only serialize the diff and its index but thats overkill for a pretty niche case anyway.
|
||||
if (pokemon.baseMoveSlots.length !== pokemon.moveSlots.length ||
|
||||
!pokemon.baseMoveSlots.every((ms, i) => ms === pokemon.moveSlots[i])) {
|
||||
state.baseMoveSlots = this.serializeWithRefs(pokemon.baseMoveSlots, pokemon.battle);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private deserializePokemon(state: /* Pokemon */ AnyObject, pokemon: Pokemon) {
|
||||
this.deserialize(state, pokemon, POKEMON, pokemon.battle);
|
||||
// @ts-ignore - readonly
|
||||
pokemon.set = state.set;
|
||||
// baseMoveSlots and moveSlots need to point to the same objects (ie. identity, not equality).
|
||||
// If we serialized the baseMoveSlots, replace any that match moveSlots to preserve the
|
||||
// identity relationship requirement.
|
||||
let baseMoveSlots;
|
||||
if (state.baseMoveSlots) {
|
||||
baseMoveSlots = this.deserializeWithRefs(state.baseMoveSlots, pokemon.battle);
|
||||
for (const [i, baseMoveSlot] of baseMoveSlots.entries()) {
|
||||
const moveSlot = pokemon.moveSlots[i];
|
||||
if (moveSlot.id === baseMoveSlot.id && !moveSlot.virtual) {
|
||||
baseMoveSlots[i] = moveSlot;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
baseMoveSlots = pokemon.moveSlots.slice();
|
||||
}
|
||||
// @ts-ignore - readonly
|
||||
pokemon.baseMoveSlots = baseMoveSlots;
|
||||
if (state.showCure === undefined) pokemon.showCure = undefined;
|
||||
}
|
||||
|
||||
private serializeChoice(choice: Choice, battle: Battle): /* Choice */ AnyObject {
|
||||
const state: /* Choice */ AnyObject = this.serialize(choice, CHOICE, battle);
|
||||
state.switchIns = Array.from(choice.switchIns);
|
||||
return state;
|
||||
}
|
||||
|
||||
private deserializeChoice(state: /* Choice */ AnyObject, choice: Choice, battle: Battle) {
|
||||
this.deserialize(state, choice, CHOICE, battle);
|
||||
choice.switchIns = new Set(state.switchIns);
|
||||
}
|
||||
|
||||
// Simply looking for a 'hit' field to determine if an object is an ActiveMove or not seems
|
||||
// pretty fragile, but its no different than what the simulator is doing. We go further and
|
||||
// also check if the object has an 'id', as that's what we will intrepret as the Move.
|
||||
private isActiveMove(obj: AnyObject): obj is ActiveMove {
|
||||
return obj.hasOwnProperty('hit') && (obj.hasOwnProperty('id') || obj.hasOwnProperty('move'));
|
||||
}
|
||||
|
||||
// ActiveMove is somewhat problematic (#5415) as it sometimes extends a Move and adds on
|
||||
// some mutable fields. We'd like to avoid displaying all the readonly fields of Move
|
||||
// (which in theory should not be changed by the ActiveMove...), so we collapse them
|
||||
// into a 'move: [Move:...]' reference. If isActiveMove returns a false positive *and*
|
||||
// and object contains an 'id' field matching a Move *and* it contains fields with the
|
||||
// same name as said Move then we'll miss them during serialization and won't
|
||||
// deserialize properly. This is unlikely to be the case, and would probably indicate
|
||||
// a bug in the simulator if it ever happened, but if not, the isActiveMove check can
|
||||
// be extended.
|
||||
private serializeActiveMove(move: ActiveMove, battle: Battle): /* ActiveMove */ AnyObject {
|
||||
const base = battle.getMove(move.id);
|
||||
const skip = new Set([...ACTIVE_MOVE]);
|
||||
for (const [key, value] of Object.entries(base)) {
|
||||
// This should really be a deepEquals check to see if anything on ActiveMove was
|
||||
// modified from the base Move, but that ends up being expensive and mostly unnecessary
|
||||
// as ActiveMove currently only mutates its simple fields (eg. `type`, `target`) anyway.
|
||||
// @ts-ignore - index signature
|
||||
if (typeof value === 'object' || move[key] === value) skip.add(key);
|
||||
}
|
||||
const state: /* ActiveMove */ AnyObject = this.serialize(move, skip, battle);
|
||||
state.move = `[Move:${move.id}]`;
|
||||
return state;
|
||||
}
|
||||
|
||||
private deserializeActiveMove(state: /* ActiveMove */ AnyObject, battle: Battle): ActiveMove {
|
||||
const move = battle.getActiveMove(this.fromRef(state.move, battle)! as Move);
|
||||
this.deserialize(state, move, ACTIVE_MOVE, battle);
|
||||
return move;
|
||||
}
|
||||
|
||||
private serializeWithRefs(obj: unknown, battle: Battle): unknown {
|
||||
switch (typeof obj) {
|
||||
case 'function':
|
||||
return undefined; // elide functions
|
||||
case 'undefined':
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'string':
|
||||
return obj;
|
||||
case 'object':
|
||||
if (obj === null) return null;
|
||||
if (Array.isArray(obj)) {
|
||||
const arr = new Array(obj.length);
|
||||
for (const [i, o] of obj.entries()) {
|
||||
arr[i] = this.serializeWithRefs(o, battle);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
if (this.isActiveMove(obj)) return this.serializeActiveMove(obj, battle);
|
||||
if (this.isReferable(obj)) return this.toRef(obj);
|
||||
if (obj.constructor !== Object) {
|
||||
// If we're getting this error, some 'special' field has been added to
|
||||
// an object and we need to update the logic in this file to handle it.
|
||||
// The most common case it that someone added a Set/Map which probably
|
||||
// needs to be serialized as an Array/Object respectively - see how
|
||||
// Battle 'hints' or Choice 'switchIns' are handled (and you will likely
|
||||
// need to add the new field to the respective skip constant).
|
||||
throw new TypeError(`Unsupported type ${obj.constructor.name}: ${obj}`);
|
||||
}
|
||||
|
||||
const o: any = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
o[key] = this.serializeWithRefs(value, battle);
|
||||
}
|
||||
return o;
|
||||
default:
|
||||
throw new TypeError(`Unexpected typeof === '${typeof obj}': ${obj}`);
|
||||
}
|
||||
}
|
||||
|
||||
private deserializeWithRefs(obj: unknown, battle: Battle) {
|
||||
switch (typeof obj) {
|
||||
case 'undefined':
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
return obj;
|
||||
case 'string':
|
||||
return this.fromRef(obj, battle) || obj;
|
||||
case 'object':
|
||||
if (obj === null) return null;
|
||||
if (Array.isArray(obj)) {
|
||||
const arr = new Array(obj.length);
|
||||
for (const [i, o] of obj.entries()) {
|
||||
arr[i] = this.deserializeWithRefs(o, battle);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
if (this.isActiveMove(obj)) return this.deserializeActiveMove(obj, battle);
|
||||
|
||||
const o: any = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
o[key] = this.deserializeWithRefs(value, battle);
|
||||
}
|
||||
return o;
|
||||
case 'function': // lol wtf
|
||||
default:
|
||||
throw new TypeError(`Unexpected typeof === '${typeof obj}': ${obj}`);
|
||||
}
|
||||
}
|
||||
|
||||
private isReferable(obj: object): obj is Referable {
|
||||
// NOTE: see explanation on the declaration above for why this must be defined lazily.
|
||||
if (!this.REFERABLE) {
|
||||
this.REFERABLE = new Set([
|
||||
Battle, Field, Side, Pokemon, Data.PureEffect,
|
||||
Data.Ability, Data.Item, Data.Move, Data.Template,
|
||||
]);
|
||||
}
|
||||
return this.REFERABLE!.has(obj.constructor);
|
||||
}
|
||||
|
||||
private toRef(obj: Referable): string {
|
||||
// Pokemon's 'id' is not only more verbose than a position, it also isn't guaranteed
|
||||
// to be uniquely identifying in custom games without Nickname/Species Clause.
|
||||
const id = obj instanceof Pokemon ? `${obj.side.id}${POSITIONS[obj.position]}` : `${obj.id}`;
|
||||
return `[${obj.constructor.name}${id ? ':' : ''}${id}]`;
|
||||
}
|
||||
|
||||
private fromRef(ref: string, battle: Battle): Referable | undefined {
|
||||
// References are sort of fragile - we're mostly just counting on there
|
||||
// being a low chance that some string field in a simulator object will not
|
||||
// 'look' like one. However, it also needs to match one of the Referable
|
||||
// class types to be decode, so we're probably OK. We could make the reference
|
||||
// markers more esoteric with additional sigils etc to avoid collisions, but
|
||||
// we're making a conscious decision to favor readability over robustness.
|
||||
if (ref.charAt(0) !== '[' && ref.slice(-1) !== ']') return undefined;
|
||||
|
||||
ref = ref.substring(1, ref.length - 1);
|
||||
// There's only one instance of these thus they don't need an id to differentiate.
|
||||
if (ref === 'Battle') return battle;
|
||||
if (ref === 'Field') return battle.field;
|
||||
|
||||
const [type, id] = ref.split(':');
|
||||
switch (type) {
|
||||
case 'Side': return battle.sides[Number(id[1]) - 1];
|
||||
case 'Pokemon': return battle.sides[Number(id[1]) - 1].pokemon[POSITIONS.indexOf(id[2])];
|
||||
case 'Ability': return battle.getAbility(id);
|
||||
case 'Item': return battle.getItem(id);
|
||||
case 'Move': return battle.getMove(id);
|
||||
case 'PureEffect': return battle.getEffect(id);
|
||||
case 'Template': return battle.getTemplate(id);
|
||||
default: return undefined; // maybe we actually got unlucky and its a string
|
||||
}
|
||||
}
|
||||
|
||||
private serialize(obj: object, skip: Set<string>, battle: Battle): AnyObject {
|
||||
const state: AnyObject = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (skip.has(key)) continue;
|
||||
const val = this.serializeWithRefs(value, battle);
|
||||
// JSON.stringify will get rid of keys with undefined values anyway, but
|
||||
// we also do it here so that assert.deepStrictEqual works on battle.toJSON().
|
||||
if (typeof val !== 'undefined') state[key] = val;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private deserialize(state: AnyObject, obj: object, skip: Set<string>, battle: Battle) {
|
||||
for (const [key, value] of Object.entries(state)) {
|
||||
if (skip.has(key)) continue;
|
||||
// @ts-ignore - index signature
|
||||
obj[key] = this.deserializeWithRefs(value, battle);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -26,6 +26,7 @@ export interface ExhaustiveRunnerOptions {
|
||||
log?: boolean;
|
||||
maxGames?: number;
|
||||
maxFailures?: number;
|
||||
dual?: boolean | 'debug';
|
||||
}
|
||||
|
||||
export class ExhaustiveRunner {
|
||||
@@ -49,6 +50,7 @@ export class ExhaustiveRunner {
|
||||
private readonly log: boolean;
|
||||
private readonly maxGames?: number;
|
||||
private readonly maxFailures?: number;
|
||||
private readonly dual: boolean | 'debug';
|
||||
|
||||
private failures: number;
|
||||
private games: number;
|
||||
@@ -61,6 +63,7 @@ export class ExhaustiveRunner {
|
||||
this.log = !!options.log;
|
||||
this.maxGames = options.maxGames;
|
||||
this.maxFailures = options.maxFailures || ExhaustiveRunner.MAX_FAILURES;
|
||||
this.dual = options.dual || false;
|
||||
|
||||
this.failures = 0;
|
||||
this.games = 0;
|
||||
@@ -84,6 +87,7 @@ export class ExhaustiveRunner {
|
||||
p1options: {team: generator.generate(), createAI},
|
||||
p2options: {team: generator.generate(), createAI},
|
||||
format: this.format,
|
||||
dual: this.dual,
|
||||
error: true,
|
||||
}).run();
|
||||
|
||||
@@ -92,7 +96,7 @@ export class ExhaustiveRunner {
|
||||
this.failures++;
|
||||
console.error(
|
||||
`\n\nRun \`node tools/simulate exhaustive --cycles=${this.cycles} ` +
|
||||
`--format=${this.format} --seed${seed.join()}\`:\n`, err);
|
||||
`--format=${this.format} --seed=${seed.join()}\`:\n`, err);
|
||||
}
|
||||
} while ((!this.maxGames || this.games < this.maxGames) &&
|
||||
(!this.maxFailures || this.failures < this.maxFailures) &&
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import assert = require('assert');
|
||||
import fs = require('fs');
|
||||
|
||||
import {ObjectReadWriteStream} from '../../lib/streams';
|
||||
import {Battle} from '../battle';
|
||||
import * as BattleStreams from '../battle-stream';
|
||||
import {PRNG, PRNGSeed} from '../prng';
|
||||
import {RandomPlayerAI} from './random-player-ai';
|
||||
@@ -26,6 +30,7 @@ export interface RunnerOptions {
|
||||
input?: boolean;
|
||||
output?: boolean;
|
||||
error?: boolean;
|
||||
dual?: boolean | 'debug';
|
||||
}
|
||||
|
||||
export class Runner {
|
||||
@@ -42,6 +47,7 @@ export class Runner {
|
||||
private readonly input: boolean;
|
||||
private readonly output: boolean;
|
||||
private readonly error: boolean;
|
||||
private readonly dual: boolean | 'debug';
|
||||
|
||||
constructor(options: RunnerOptions) {
|
||||
this.format = options.format;
|
||||
@@ -54,10 +60,13 @@ export class Runner {
|
||||
this.input = !!options.input;
|
||||
this.output = !!options.output;
|
||||
this.error = !!options.error;
|
||||
this.dual = options.dual || false;
|
||||
}
|
||||
|
||||
async run() {
|
||||
const battleStream = new RawBattleStream(this.input);
|
||||
const battleStream = this.dual ?
|
||||
new DualStream(this.input, this.dual === 'debug') :
|
||||
new RawBattleStream(this.input);
|
||||
const game = this.runGame(this.format, battleStream);
|
||||
if (!this.error) return game;
|
||||
return game.catch(err => {
|
||||
@@ -66,7 +75,8 @@ export class Runner {
|
||||
});
|
||||
}
|
||||
|
||||
private async runGame(format: string, battleStream: RawBattleStream) {
|
||||
private async runGame(format: string, battleStream: RawBattleStream | DualStream) {
|
||||
// @ts-ignore - DualStream implements everything relevant from BattleStream.
|
||||
const streams = BattleStreams.getPlayerStreams(battleStream);
|
||||
const spec = {formatid: format, seed: this.prng.seed};
|
||||
const p1spec = this.getPlayerSpec("Bot 1", this.p1options);
|
||||
@@ -129,3 +139,68 @@ class RawBattleStream extends BattleStreams.BattleStream {
|
||||
super._write(message);
|
||||
}
|
||||
}
|
||||
|
||||
class DualStream {
|
||||
private debug: boolean;
|
||||
private readonly control: RawBattleStream;
|
||||
private test: RawBattleStream;
|
||||
|
||||
constructor(input: boolean, debug: boolean) {
|
||||
this.debug = debug;
|
||||
// The input to both streams should be the same, so to satisfy the
|
||||
// input flag we only need to track the raw input of one stream.
|
||||
this.control = new RawBattleStream(input);
|
||||
this.test = new RawBattleStream(false);
|
||||
}
|
||||
|
||||
get rawInputLog() {
|
||||
const control = this.control.rawInputLog;
|
||||
const test = this.test.rawInputLog;
|
||||
assert.deepStrictEqual(test, control);
|
||||
return control;
|
||||
}
|
||||
|
||||
async read() {
|
||||
const control = await this.control.read();
|
||||
const test = await this.test.read();
|
||||
// In debug mode, wait to catch this as a difference in the inputLog
|
||||
// and error there so we get the full battle state dumped instead.
|
||||
if (!this.debug) assert.strictEqual(test, control);
|
||||
return control;
|
||||
}
|
||||
|
||||
write(message: string) {
|
||||
this.control._write(message);
|
||||
this.test._write(message);
|
||||
this.compare();
|
||||
}
|
||||
|
||||
async end() {
|
||||
// We need to compare first because _end() destroys the battle object.
|
||||
this.compare(true);
|
||||
await this.control._end();
|
||||
await this.test._end();
|
||||
}
|
||||
|
||||
compare(end?: boolean) {
|
||||
if (!this.control.battle || !this.test.battle) return;
|
||||
|
||||
const control = this.control.battle.toJSON();
|
||||
const test = this.test.battle.toJSON();
|
||||
try {
|
||||
assert.deepStrictEqual(test, control);
|
||||
} catch (err) {
|
||||
if (this.debug) {
|
||||
// NOTE: diffing these directly won't work because the key ordering isn't stable.
|
||||
fs.writeFileSync('logs/control.json', JSON.stringify(control, null, 2));
|
||||
fs.writeFileSync('logs/test.json', JSON.stringify(test, null, 2));
|
||||
}
|
||||
throw new Error(err.message);
|
||||
}
|
||||
|
||||
if (end) return;
|
||||
const send = this.test.battle.send;
|
||||
this.test.battle = Battle.fromJSON(test);
|
||||
this.test.battle.restart(send);
|
||||
}
|
||||
}
|
||||
|
||||
50
test/sim/misc/state.js
Normal file
50
test/sim/misc/state.js
Normal file
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('./../../assert');
|
||||
const common = require('./../../common');
|
||||
const Battle = require('./../../../.sim-dist/battle').Battle;
|
||||
|
||||
const TEAMS = [[
|
||||
{species: 'Mew', ability: 'synchronize', item: 'assaultvest', moves: ['psychic']},
|
||||
{species: 'Ditto', ability: 'imposter', item: 'choicescarf', moves: ['transform']},
|
||||
{species: 'Amoonguss', ability: 'effectspore', item: 'blacksludge', moves: ['toxic']},
|
||||
{species: 'Gliscor', ability: 'poisonheal', item: 'toxicorb', moves: ['curse']},
|
||||
{species: 'Zoroark', ability: 'illusion', item: 'leftovers', moves: ['knockoff']},
|
||||
{species: 'Gengar', ability: 'cursedbody', item: 'brightpowder', moves: ['disable']},
|
||||
], [
|
||||
{species: 'Ninjask', ability: 'speedboost', item: 'rockyhelmet', moves: ['batonpass']},
|
||||
{species: 'Hippowdon', ability: 'sandstream', item: 'choiceband', moves: ['earthquake']},
|
||||
{species: 'Jirachi', ability: 'serenegrace', item: 'choicescarf', moves: ['ironhead']},
|
||||
{species: 'Chansey', ability: 'naturalcure', item: 'eviolite', moves: ['seismictoss']},
|
||||
{species: 'Vaporeon', ability: 'waterabsorb', item: 'wacanberry', moves: ['surf']},
|
||||
{species: 'Snorlax', ability: 'thickfat', item: 'leftovers', moves: ['rest']},
|
||||
]];
|
||||
|
||||
describe('State', function () {
|
||||
describe('Battles', function () {
|
||||
it('should be able to be serialized and deserialized without affecting functionality', function () {
|
||||
const control = common.createBattle(TEAMS);
|
||||
let test = common.createBattle(TEAMS);
|
||||
|
||||
while (!(control.ended || test.ended)) {
|
||||
control.makeChoices();
|
||||
test.makeChoices();
|
||||
|
||||
assert.deepStrictEqual(test.toJSON(), control.toJSON());
|
||||
|
||||
// Roundtrip the test battle to confirm it still works.
|
||||
const send = test.send;
|
||||
test = Battle.fromJSON(JSON.stringify(test));
|
||||
test.restart(send);
|
||||
}
|
||||
|
||||
control.destroy();
|
||||
test.destroy();
|
||||
});
|
||||
it('should require special treatment for complex objects', function () {
|
||||
const battle = common.createBattle(TEAMS);
|
||||
battle.foo = new Map();
|
||||
assert.throws(() => battle.toJSON(), /Unsupported type Map/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -68,7 +68,7 @@ const RejectionTracker = new class {
|
||||
for (const u of this.unhandled) {
|
||||
const error = (u.reason instanceof Error) ? u.reason :
|
||||
new Error(`Promise rejected with value: ${u.reason}`);
|
||||
console.error(error.stack);
|
||||
console.error(`UNHANDLED PROMISE REJECTION:\n${error.stack}`);
|
||||
i++;
|
||||
}
|
||||
process.exit(code + i);
|
||||
@@ -136,7 +136,7 @@ case 'exhaustive':
|
||||
do {
|
||||
for (let format of formats) {
|
||||
failures += await new ExhaustiveRunner({
|
||||
format, cycles, prng, maxFailures, log: true,
|
||||
format, cycles, prng, maxFailures, log: true, dual: argv.dual,
|
||||
}).run();
|
||||
process.stdout.write('\n');
|
||||
if (failures >= maxFailures) break;
|
||||
|
||||
Reference in New Issue
Block a user