mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
Remove |callback| in favor of |error| (#5418)
This commit is contained in:
parent
e1c389a182
commit
b2777f9bf6
|
|
@ -673,9 +673,15 @@ battle will continue.
|
|||
If an invalid decision is sent (trying to switch when you're trapped by
|
||||
Mean Look or something), you will receive a message starting with:
|
||||
|
||||
`|error|[Invalid choice]`
|
||||
`|error|[Invalid choice] MESSAGE`
|
||||
|
||||
This will tell you to send a different decision.
|
||||
This will tell you to send a different decision. If your previous choice
|
||||
revealed additional information (For example: a move disabled by Imprison
|
||||
or a trapping effect), the error will be followed with a `|request|` command
|
||||
to base your decision off of:
|
||||
|
||||
`|error|[Unavailable choice] MESSAGE`
|
||||
`|request|REQUEST`
|
||||
|
||||
### Choice requests
|
||||
|
||||
|
|
|
|||
|
|
@ -271,20 +271,12 @@ export abstract class BattlePlayer {
|
|||
if (this.debug) console.log(line);
|
||||
if (line.charAt(0) !== '|') return;
|
||||
const [cmd, rest] = splitFirst(line.slice(1), '|');
|
||||
if (cmd === 'request') {
|
||||
return this.receiveRequest(JSON.parse(rest));
|
||||
}
|
||||
if (cmd === 'callback') {
|
||||
return this.receiveCallback(rest.split('|'));
|
||||
}
|
||||
if (cmd === 'error') {
|
||||
return this.receiveError(new Error(rest));
|
||||
}
|
||||
if (cmd === 'request') return this.receiveRequest(JSON.parse(rest));
|
||||
if (cmd === 'error') return this.receiveError(new Error(rest));
|
||||
this.log.push(line);
|
||||
}
|
||||
|
||||
abstract receiveRequest(request: AnyObject): void;
|
||||
abstract receiveCallback(callback: string[]): void;
|
||||
|
||||
receiveError(error: Error) {
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -15,11 +15,6 @@ export class RandomPlayerAI extends BattlePlayer {
|
|||
protected readonly mega: number;
|
||||
protected readonly prng: PRNG;
|
||||
|
||||
trapped: Set<number>;
|
||||
disabled: Map<number, Set<string>>;
|
||||
lastRequest?: AnyObject;
|
||||
retry: boolean;
|
||||
|
||||
constructor(
|
||||
playerStream: ObjectReadWriteStream<string>,
|
||||
options: {move?: number, mega?: number, seed?: PRNG | PRNGSeed | null } = {},
|
||||
|
|
@ -29,53 +24,16 @@ export class RandomPlayerAI extends BattlePlayer {
|
|||
this.move = options.move || 1.0;
|
||||
this.mega = options.mega || 0;
|
||||
this.prng = options.seed && !Array.isArray(options.seed) ? options.seed : new PRNG(options.seed);
|
||||
|
||||
this.disabled = new Map();
|
||||
this.trapped = new Set();
|
||||
this.retry = false;
|
||||
}
|
||||
|
||||
receiveError(error: Error) {
|
||||
if (error.message.startsWith(`[Invalid choice]`) && this.retry) {
|
||||
this.retry = false;
|
||||
// If we get a choice error, we retry the choice using the last request provided
|
||||
// we've got a '|callback' updating our state regarding what Pokemon are trapped
|
||||
// or disabled.
|
||||
this.makeChoice(this.lastRequest!);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
// If we made an unavailable choice we will receive a followup request to
|
||||
// allow us the opportunity to correct our decision.
|
||||
if (error.message.startsWith('[Unavailable choice]')) return;
|
||||
throw error;
|
||||
}
|
||||
|
||||
receiveRequest(request: AnyObject) {
|
||||
this.disabled = new Map();
|
||||
this.trapped = new Set();
|
||||
this.retry = false;
|
||||
|
||||
this.lastRequest = request;
|
||||
this.makeChoice(request);
|
||||
}
|
||||
|
||||
receiveCallback(callback: string[]) {
|
||||
const [type, ...args] = callback;
|
||||
if (type === 'cant') {
|
||||
this.retry = true;
|
||||
const [pokemon, _, move] = args;
|
||||
const position = pokemon[2].indexOf(`abcdef`);
|
||||
let moves = this.disabled.get(position);
|
||||
if (!moves) {
|
||||
moves = new Set();
|
||||
this.disabled.set(position, moves);
|
||||
}
|
||||
moves.add(move);
|
||||
} else if (type === 'trapped') {
|
||||
this.retry = true;
|
||||
const position = Number(args);
|
||||
this.trapped.add(position);
|
||||
}
|
||||
}
|
||||
|
||||
makeChoice(request: AnyObject) {
|
||||
if (request.wait) {
|
||||
// wait request
|
||||
// do nothing
|
||||
|
|
@ -116,11 +74,9 @@ export class RandomPlayerAI extends BattlePlayer {
|
|||
canUltraBurst = canUltraBurst && active.canUltraBurst;
|
||||
canZMove = canZMove && !!active.canZMove;
|
||||
|
||||
const disabled = this.disabled.get(i);
|
||||
let canMove = [1, 2, 3, 4].slice(0, active.moves.length).filter(j => (
|
||||
// not disabled
|
||||
!active.moves[j - 1].disabled &&
|
||||
(!disabled || !disabled.has(toId(active.moves[j - 1].move)))
|
||||
!active.moves[j - 1].disabled
|
||||
// NOTE: we don't actually check for whether we have PP or not because the
|
||||
// simulator will mark the move as disabled if there is zero PP and there are
|
||||
// situations where we actually need to use a move with 0 PP (Gen 1 Wrap).
|
||||
|
|
@ -178,8 +134,7 @@ export class RandomPlayerAI extends BattlePlayer {
|
|||
// not fainted
|
||||
!pokemon[j - 1].condition.endsWith(` fnt`)
|
||||
));
|
||||
const trapped = active.trapped || (this.trapped && this.trapped.has(i));
|
||||
const switches = trapped ? [] : canSwitch;
|
||||
const switches = active.trapped ? [] : canSwitch;
|
||||
|
||||
if (switches.length && (!moves.length || this.prng.next() > this.move)) {
|
||||
const target = this.chooseSwitch(
|
||||
|
|
|
|||
62
sim/side.ts
62
sim/side.ts
|
|
@ -314,19 +314,16 @@ export class Side {
|
|||
this.battle.send('sideupdate', `${this.id}\n${sideUpdate}`);
|
||||
}
|
||||
|
||||
emitCallback(...args: (string | number | AnyObject)[]) {
|
||||
this.battle.send('sideupdate', `${this.id}\n|callback|${args.join('|')}`);
|
||||
}
|
||||
|
||||
emitRequest(update: AnyObject) {
|
||||
this.battle.send('sideupdate', `${this.id}\n|request|${JSON.stringify(update)}`);
|
||||
this.activeRequest = update;
|
||||
}
|
||||
|
||||
emitChoiceError(message: string) {
|
||||
emitChoiceError(message: string, unavailable?: boolean) {
|
||||
this.choice.error = message;
|
||||
this.battle.send('sideupdate', `${this.id}\n|error|[Invalid choice] ${message}`);
|
||||
if (this.battle.strictChoices) throw new Error(`[Invalid choice] ${message}`);
|
||||
const type = `[${unavailable ? 'Unavailable' : 'Invalid'} choice]`;
|
||||
this.battle.send('sideupdate', `${this.id}\n|error|${type} ${message}`);
|
||||
if (this.battle.strictChoices) throw new Error(`${type} ${message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -408,7 +405,6 @@ export class Side {
|
|||
return this.emitChoiceError(`Can't move: ${pokemon.name} can't use ${move.name} as a Z-move`);
|
||||
}
|
||||
if (zMove && this.choice.zMove) {
|
||||
this.emitCallback('cantz', pokemon);
|
||||
return this.emitChoiceError(`Can't move: You can't Z-move more than once per battle`);
|
||||
}
|
||||
|
||||
|
|
@ -462,8 +458,26 @@ export class Side {
|
|||
if (!isEnabled) {
|
||||
// Request a different choice
|
||||
if (autoChoose) throw new Error(`autoChoose chose a disabled move`);
|
||||
this.emitCallback('cant', pokemon, disabledSource, moveid);
|
||||
return this.emitChoiceError(`Can't move: ${pokemon.name}'s ${move.name} is disabled`);
|
||||
const includeRequest = this.updateRequestForPokemon(pokemon, req => {
|
||||
let updated = false;
|
||||
for (const m of req.moves) {
|
||||
if (m.id === moveid) {
|
||||
if (!m.disabled) {
|
||||
m.disabled = true;
|
||||
updated = true;
|
||||
}
|
||||
if (m.disabledSource !== disabledSource) {
|
||||
m.disabledSource = disabledSource;
|
||||
updated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
const status = this.emitChoiceError(`Can't move: ${pokemon.name}'s ${move.name} is disabled`, includeRequest);
|
||||
if (includeRequest) this.emitRequest(this.activeRequest!);
|
||||
return status;
|
||||
}
|
||||
// The chosen move is valid yay
|
||||
}
|
||||
|
|
@ -475,7 +489,6 @@ export class Side {
|
|||
return this.emitChoiceError(`Can't move: ${pokemon.name} can't mega evolve`);
|
||||
}
|
||||
if (mega && this.choice.mega) {
|
||||
this.emitCallback('cantmega', pokemon);
|
||||
return this.emitChoiceError(`Can't move: You can only mega-evolve once per battle`);
|
||||
}
|
||||
const ultra = (megaOrZ === 'ultra');
|
||||
|
|
@ -483,7 +496,6 @@ export class Side {
|
|||
return this.emitChoiceError(`Can't move: ${pokemon.name} can't mega evolve`);
|
||||
}
|
||||
if (ultra && this.choice.ultra) {
|
||||
this.emitCallback('cantmega', pokemon);
|
||||
return this.emitChoiceError(`Can't move: You can only ultra burst once per battle`);
|
||||
}
|
||||
|
||||
|
|
@ -507,6 +519,15 @@ export class Side {
|
|||
return true;
|
||||
}
|
||||
|
||||
updateRequestForPokemon(pokemon: Pokemon, update: (req: AnyObject) => boolean) {
|
||||
if (!this.activeRequest || !this.activeRequest.active) {
|
||||
throw new Error(`Can't update a request without active Pokemon`);
|
||||
}
|
||||
const req = this.activeRequest.active[pokemon.position];
|
||||
if (!req) throw new Error(`Pokemon not found in request's active field`);
|
||||
return update(req);
|
||||
}
|
||||
|
||||
chooseSwitch(slotText?: string) {
|
||||
if (this.requestState !== 'move' && this.requestState !== 'switch') {
|
||||
return this.emitChoiceError(`Can't switch: You need a ${this.requestState} response`);
|
||||
|
|
@ -559,8 +580,21 @@ export class Side {
|
|||
|
||||
if (this.requestState === 'move') {
|
||||
if (pokemon.trapped) {
|
||||
this.emitCallback('trapped', pokemon.position);
|
||||
return this.emitChoiceError(`Can't switch: The active Pokémon is trapped`);
|
||||
const includeRequest = this.updateRequestForPokemon(pokemon, req => {
|
||||
let updated = false;
|
||||
if (req.maybeTrapped) {
|
||||
delete req.maybeTrapped;
|
||||
updated = true;
|
||||
}
|
||||
if (!req.trapped) {
|
||||
req.trapped = true;
|
||||
updated = true;
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
const status = this.emitChoiceError(`Can't switch: The active Pokémon is trapped`, includeRequest);
|
||||
if (includeRequest) this.emitRequest(this.activeRequest!);
|
||||
return status;
|
||||
} else if (pokemon.maybeTrapped) {
|
||||
this.choice.cantUndo = this.choice.cantUndo || pokemon.isLastActive();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,17 +74,17 @@ assert.holdsItem = function (pokemon, message) {
|
|||
});
|
||||
};
|
||||
|
||||
assert.trapped = function (fn, message) {
|
||||
assert.trapped = function (fn, unavailable, message) {
|
||||
assert.throws(
|
||||
fn, /\[Invalid choice\] Can't switch: The active Pokémon is trapped/,
|
||||
fn, new RegExp(`\\[${unavailable ? 'Unavailable' : 'Invalid'} choice\\] Can't switch: The active Pokémon is trapped`),
|
||||
message || 'Expected active Pokemon to be trapped.');
|
||||
};
|
||||
|
||||
assert.cantMove = function (fn, pokemon, move, message) {
|
||||
assert.cantMove = function (fn, pokemon, move, unavailable, message) {
|
||||
message = message || `Expected ${pokemon} to not be able to use ${move}.`;
|
||||
if (pokemon && move) {
|
||||
assert.throws(
|
||||
fn, new RegExp(`\\[Invalid choice\\] Can't move:.*${pokemon}.*${move}`, 'i'), message);
|
||||
fn, new RegExp(`\\[${unavailable ? 'Unavailable' : 'Invalid'} choice\\] Can't move:.*${pokemon}.*${move}`, 'i'), message);
|
||||
} else {
|
||||
assert.throws(fn, /\[Invalid choice\] Can't move:/, message);
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ assert.cantUndo = function (fn, message) {
|
|||
};
|
||||
|
||||
assert.cantTarget = function (fn, move, message) {
|
||||
assert.cantMove(fn, 'target', move, message || `Expected not to be able to choose a target for ${move}.`);
|
||||
assert.cantMove(fn, 'target', move, false, message || `Expected not to be able to choose a target for ${move}.`);
|
||||
};
|
||||
|
||||
assert.statStage = function (pokemon, statName, stage, message) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ describe('Arena Trap', function () {
|
|||
assert.species(p2active[0], 'Dusknoir');
|
||||
battle.makeChoices('move snore', 'switch 5');
|
||||
assert.species(p2active[0], 'Magnezone');
|
||||
assert.trapped(() => battle.makeChoices('', 'switch 6'));
|
||||
assert.trapped(() => battle.makeChoices('', 'switch 6'), true);
|
||||
|
||||
assert.species(p2active[0], 'Magnezone'); // Magnezone is trapped
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ describe('Arena Trap', function () {
|
|||
battle.makeChoices('move snore', 'switch 6');
|
||||
assert.species(p2active[0], 'Vaporeon');
|
||||
|
||||
assert.trapped(() => battle.makeChoices('default', 'switch 2')); // Vaporeon is trapped
|
||||
assert.trapped(() => battle.makeChoices('default', 'switch 2'), true); // Vaporeon is trapped
|
||||
assert.species(p2active[0], 'Vaporeon');
|
||||
|
||||
battle.makeChoices('move telekinesis', 'default'); // Telekinesis
|
||||
|
|
@ -51,7 +51,7 @@ describe('Arena Trap', function () {
|
|||
|
||||
battle.makeChoices('move gravity', 'default'); // Gravity
|
||||
|
||||
assert.trapped(() => battle.makeChoices('', 'switch 4')); // Tornadus is trapped
|
||||
assert.trapped(() => battle.makeChoices('', 'switch 4'), true); // Tornadus is trapped
|
||||
assert.species(p2active[0], 'Tornadus');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ describe('Magnet Pull', function () {
|
|||
{species: "Starmie", ability: 'illuminate', moves: ['reflecttype']},
|
||||
]});
|
||||
|
||||
assert.trapped(() => battle.makeChoices('', 'switch 2'));
|
||||
assert.trapped(() => battle.makeChoices('', 'switch 2'), true);
|
||||
battle.makeChoices('auto', 'auto');
|
||||
assert.species(battle.p2.active[0], 'Heatran');
|
||||
battle.makeChoices('auto', 'switch 2');
|
||||
assert.species(battle.p2.active[0], 'Starmie');
|
||||
battle.makeChoices('move charge', 'move reflecttype'); // Reflect Type makes Starmie part Steel
|
||||
assert.trapped(() => battle.makeChoices('', 'switch 2'));
|
||||
assert.trapped(() => battle.makeChoices('', 'switch 2'), true);
|
||||
battle.makeChoices('auto', 'auto');
|
||||
assert.species(battle.p2.active[0], 'Starmie');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ describe('Shadow Tag', function () {
|
|||
{species: "Tornadus", ability: 'defiant', moves: ['tailwind']},
|
||||
{species: "Heatran", ability: 'flashfire', moves: ['roar']},
|
||||
]});
|
||||
assert.trapped(() => battle.makeChoices('move counter', 'switch 2'));
|
||||
assert.trapped(() => battle.makeChoices('move counter', 'switch 2'), true);
|
||||
});
|
||||
|
||||
it('should not prevent Pokemon from switching out using moves', function () {
|
||||
|
|
|
|||
|
|
@ -300,11 +300,11 @@ describe('Choices', function () {
|
|||
it('should not force Struggle usage on move attempt when choosing a disabled move', function () {
|
||||
battle = common.createBattle();
|
||||
battle.setPlayer('p1', {team: [{species: "Mew", item: 'assaultvest', ability: 'synchronize', moves: ['recover', 'icebeam']}]});
|
||||
battle.setPlayer('p2', {team: [{species: "Rhydon", item: '', ability: 'prankster', moves: ['struggle', 'surf']}]});
|
||||
battle.setPlayer('p2', {team: [{species: "Rhydon", item: '', ability: 'prankster', moves: ['surf']}]});
|
||||
const failingAttacker = battle.p1.active[0];
|
||||
battle.p2.chooseMove(2);
|
||||
battle.p2.chooseMove(1);
|
||||
|
||||
assert.cantMove(() => battle.p1.chooseMove(1), 'Mew', 'Recover');
|
||||
assert.cantMove(() => battle.p1.chooseMove(1), 'Mew', 'Recover', true);
|
||||
assert.strictEqual(battle.turn, 1);
|
||||
assert.notStrictEqual(failingAttacker.lastMove && failingAttacker.lastMove.id, 'struggle');
|
||||
|
||||
|
|
@ -314,7 +314,7 @@ describe('Choices', function () {
|
|||
});
|
||||
|
||||
it('should send meaningful feedback to players if they try to use a disabled move', function () {
|
||||
battle = common.createBattle();
|
||||
battle = common.createBattle({strictChoices: false});
|
||||
battle.setPlayer('p1', {team: [{species: "Skarmory", ability: 'sturdy', moves: ['spikes', 'roost']}]});
|
||||
battle.setPlayer('p2', {team: [{species: "Smeargle", ability: 'owntempo', moves: ['imprison', 'spikes']}]});
|
||||
|
||||
|
|
@ -324,15 +324,14 @@ describe('Choices', function () {
|
|||
battle.send = (type, data) => {
|
||||
if (type === 'sideupdate') buffer.push(Array.isArray(data) ? data.join('\n') : data);
|
||||
};
|
||||
assert.cantMove(() => battle.makeChoices('move 1', 'default'), 'Skarmory', 'Spikes');
|
||||
assert(buffer.length >= 1);
|
||||
assert(buffer.some(message => {
|
||||
return message.startsWith('p1\n') && /\bcant\b/.test(message) && (/\|0\b/.test(message) || /\|p1a\b/.test(message));
|
||||
}));
|
||||
battle.p1.chooseMove(1);
|
||||
assert(buffer.length >= 2);
|
||||
assert(buffer.some(message => message.startsWith('p1\n|error|[Unavailable choice]')));
|
||||
assert(buffer.some(message => message.startsWith('p1\n|request|') && JSON.parse(message.slice(12)).active[0].moves[0].disabled));
|
||||
});
|
||||
|
||||
it('should send meaningful feedback to players if they try to switch a trapped Pokémon out', function () {
|
||||
battle = common.createBattle();
|
||||
battle = common.createBattle({strictChoices: false});
|
||||
battle.setPlayer('p1', {team: [
|
||||
{species: "Scizor", ability: 'swarm', moves: ['bulletpunch']},
|
||||
{species: "Azumarill", ability: 'sapsipper', moves: ['aquajet']},
|
||||
|
|
@ -343,11 +342,10 @@ describe('Choices', function () {
|
|||
battle.send = (type, data) => {
|
||||
if (type === 'sideupdate') buffer.push(Array.isArray(data) ? data.join('\n') : data);
|
||||
};
|
||||
assert.trapped(() => battle.makeChoices('switch 2', 'default'));
|
||||
assert(buffer.length >= 1);
|
||||
assert(buffer.some(message => {
|
||||
return message.startsWith('p1\n') && /\btrapped\b/.test(message) && (/\|0\b/.test(message) || /\|p1a\b/.test(message));
|
||||
}));
|
||||
battle.p1.chooseSwitch(2);
|
||||
assert(buffer.length >= 2);
|
||||
assert(buffer.some(message => message.startsWith('p1\n|error|[Unavailable choice]')));
|
||||
assert(buffer.some(message => message.startsWith('p1\n|request|') && JSON.parse(message.slice(12)).active[0].trapped));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ describe('Imprison', function () {
|
|||
battle.makeChoices('move imprison', 'move calmmind');
|
||||
assert.statStage(battle.p2.active[0], 'spa', 0);
|
||||
assert.statStage(battle.p2.active[0], 'spd', 0);
|
||||
assert.cantMove(() => battle.choose('p2', 'move calmmind'), 'Abra', 'Calm Mind');
|
||||
assert.cantMove(() => battle.choose('p2', 'move calmmind'), 'Abra', 'Calm Mind', true);
|
||||
|
||||
// Imprison doesn't end when the foe switches
|
||||
battle.makeChoices('default', 'switch 2');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user