Skip to content

Commit

Permalink
Remove |callback| in favor of |error| (#5418)
Browse files Browse the repository at this point in the history
  • Loading branch information
scheibo authored and Zarel committed Apr 8, 2019
1 parent e1c389a commit b2777f9
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 104 deletions.
10 changes: 8 additions & 2 deletions sim/SIM-PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 2 additions & 10 deletions sim/battle-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 6 additions & 51 deletions sim/examples/random-player-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {},
Expand All @@ -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
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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(
Expand Down
62 changes: 48 additions & 14 deletions sim/side.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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`);
}

Expand Down Expand Up @@ -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
}
Expand All @@ -475,15 +489,13 @@ 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');
if (ultra && !pokemon.canUltraBurst) {
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`);
}

Expand All @@ -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`);
Expand Down Expand Up @@ -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();
}
Expand Down
10 changes: 5 additions & 5 deletions test/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions test/simulator/abilities/arenatrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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');
});
});
4 changes: 2 additions & 2 deletions test/simulator/abilities/magnetpull.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
2 changes: 1 addition & 1 deletion test/simulator/abilities/shadowtag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
28 changes: 13 additions & 15 deletions test/simulator/misc/decisions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

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

Expand All @@ -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']},
Expand All @@ -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));
});
});

Expand Down
Loading

0 comments on commit b2777f9

Please sign in to comment.