Remove |callback| in favor of |error| (#5418)

This commit is contained in:
Kirk Scheibelhut 2019-04-08 12:33:32 -07:00 committed by Guangcong Luo
parent e1c389a182
commit b2777f9bf6
10 changed files with 89 additions and 104 deletions

View File

@ -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

View File

@ -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;

View File

@ -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(

View File

@ -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();
}

View File

@ -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) {

View File

@ -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');
});
});

View File

@ -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');
});

View File

@ -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 () {

View File

@ -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));
});
});

View File

@ -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');