Spaces:
Running
Running
/** | |
* Example random player AI. | |
* | |
* Pokemon Showdown - http://pokemonshowdown.com/ | |
* | |
* @license MIT | |
*/ | |
import type { ObjectReadWriteStream } from '../../lib/streams'; | |
import { BattlePlayer } from '../battle-stream'; | |
import { PRNG, type PRNGSeed } from '../prng'; | |
import type { ChoiceRequest } from '../side'; | |
export class RandomPlayerAI extends BattlePlayer { | |
protected readonly move: number; | |
protected readonly mega: number; | |
protected readonly prng: PRNG; | |
constructor( | |
playerStream: ObjectReadWriteStream<string>, | |
options: { move?: number, mega?: number, seed?: PRNG | PRNGSeed | null } = {}, | |
debug = false | |
) { | |
super(playerStream, debug); | |
this.move = options.move || 1.0; | |
this.mega = options.mega || 0; | |
this.prng = PRNG.get(options.seed); | |
} | |
receiveError(error: 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; | |
} | |
override receiveRequest(request: ChoiceRequest) { | |
if (request.wait) { | |
// wait request | |
// do nothing | |
} else if (request.forceSwitch) { | |
// switch request | |
const pokemon = request.side.pokemon; | |
const chosen: number[] = []; | |
const choices = request.forceSwitch.map((mustSwitch, i) => { | |
if (!mustSwitch) return `pass`; | |
const canSwitch = range(1, 6).filter(j => ( | |
pokemon[j - 1] && | |
// not active | |
j > request.forceSwitch.length && | |
// not chosen for a simultaneous switch | |
!chosen.includes(j) && | |
// not fainted or fainted and using Revival Blessing | |
!pokemon[j - 1].condition.endsWith(` fnt`) === !pokemon[i].reviving | |
)); | |
if (!canSwitch.length) return `pass`; | |
const target = this.chooseSwitch( | |
undefined, | |
canSwitch.map(slot => ({ slot, pokemon: pokemon[slot - 1] })) | |
); | |
chosen.push(target); | |
return `switch ${target}`; | |
}); | |
this.choose(choices.join(`, `)); | |
} else if (request.teamPreview) { | |
this.choose(this.chooseTeamPreview(request.side.pokemon)); | |
} else if (request.active) { | |
// move request | |
let [canMegaEvo, canUltraBurst, canZMove, canDynamax, canTerastallize] = [true, true, true, true, true]; | |
const pokemon = request.side.pokemon; | |
const chosen: number[] = []; | |
const choices = request.active.map((active: AnyObject, i: number) => { | |
if (pokemon[i].condition.endsWith(` fnt`) || pokemon[i].commanding) return `pass`; | |
canMegaEvo = canMegaEvo && active.canMegaEvo; | |
canUltraBurst = canUltraBurst && active.canUltraBurst; | |
canZMove = canZMove && !!active.canZMove; | |
canDynamax = canDynamax && !!active.canDynamax; | |
canTerastallize = canTerastallize && !!active.canTerastallize; | |
// Determine whether we should change form if we do end up switching | |
const change = (canMegaEvo || canUltraBurst || canDynamax) && this.prng.random() < this.mega; | |
// If we've already dynamaxed or if we're planning on potentially dynamaxing | |
// we need to use the maxMoves instead of our regular moves | |
const useMaxMoves = (!active.canDynamax && active.maxMoves) || (change && canDynamax); | |
const possibleMoves = useMaxMoves ? active.maxMoves.maxMoves : active.moves; | |
let canMove = range(1, possibleMoves.length).filter(j => ( | |
// not disabled | |
!possibleMoves[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). | |
)).map(j => ({ | |
slot: j, | |
move: possibleMoves[j - 1].move, | |
target: possibleMoves[j - 1].target, | |
zMove: false, | |
})); | |
if (canZMove) { | |
canMove.push(...range(1, active.canZMove.length) | |
.filter(j => active.canZMove[j - 1]) | |
.map(j => ({ | |
slot: j, | |
move: active.canZMove[j - 1].move, | |
target: active.canZMove[j - 1].target, | |
zMove: true, | |
}))); | |
} | |
// Filter out adjacentAlly moves if we have no allies left, unless they're our | |
// only possible move options. | |
const hasAlly = pokemon.length > 1 && !pokemon[i ^ 1].condition.endsWith(` fnt`); | |
const filtered = canMove.filter(m => m.target !== `adjacentAlly` || hasAlly); | |
canMove = filtered.length ? filtered : canMove; | |
const moves = canMove.map(m => { | |
let move = `move ${m.slot}`; | |
// NOTE: We don't generate all possible targeting combinations. | |
if (request.active.length > 1) { | |
if ([`normal`, `any`, `adjacentFoe`].includes(m.target)) { | |
move += ` ${1 + this.prng.random(2)}`; | |
} | |
if (m.target === `adjacentAlly`) { | |
move += ` -${(i ^ 1) + 1}`; | |
} | |
if (m.target === `adjacentAllyOrSelf`) { | |
if (hasAlly) { | |
move += ` -${1 + this.prng.random(2)}`; | |
} else { | |
move += ` -${i + 1}`; | |
} | |
} | |
} | |
if (m.zMove) move += ` zmove`; | |
return { choice: move, move: m }; | |
}); | |
const canSwitch = range(1, 6).filter(j => ( | |
pokemon[j - 1] && | |
// not active | |
!pokemon[j - 1].active && | |
// not chosen for a simultaneous switch | |
!chosen.includes(j) && | |
// not fainted | |
!pokemon[j - 1].condition.endsWith(` fnt`) | |
)); | |
const switches = active.trapped ? [] : canSwitch; | |
if (switches.length && (!moves.length || this.prng.random() > this.move)) { | |
const target = this.chooseSwitch( | |
active, | |
canSwitch.map(slot => ({ slot, pokemon: pokemon[slot - 1] })) | |
); | |
chosen.push(target); | |
return `switch ${target}`; | |
} else if (moves.length) { | |
const move = this.chooseMove(active, moves); | |
if (move.endsWith(` zmove`)) { | |
canZMove = false; | |
return move; | |
} else if (change) { | |
if (canTerastallize) { | |
canTerastallize = false; | |
return `${move} terastallize`; | |
} else if (canDynamax) { | |
canDynamax = false; | |
return `${move} dynamax`; | |
} else if (canMegaEvo) { | |
canMegaEvo = false; | |
return `${move} mega`; | |
} else { | |
canUltraBurst = false; | |
return `${move} ultra`; | |
} | |
} else { | |
return move; | |
} | |
} else { | |
throw new Error(`${this.constructor.name} unable to make choice ${i}. request='${typeof request}',` + | |
` chosen='${chosen}', (mega=${canMegaEvo}, ultra=${canUltraBurst}, zmove=${canZMove},` + | |
` dynamax='${canDynamax}', terastallize=${canTerastallize})`); | |
} | |
}); | |
this.choose(choices.join(`, `)); | |
} | |
} | |
protected chooseTeamPreview(team: AnyObject[]): string { | |
return `default`; | |
} | |
protected chooseMove(active: AnyObject, moves: { choice: string, move: AnyObject }[]): string { | |
return this.prng.sample(moves).choice; | |
} | |
protected chooseSwitch(active: AnyObject | undefined, switches: { slot: number, pokemon: AnyObject }[]): number { | |
return this.prng.sample(switches).slot; | |
} | |
} | |
// Creates an array of numbers progressing from start up to and including end | |
function range(start: number, end?: number, step = 1) { | |
if (end === undefined) { | |
end = start; | |
start = 0; | |
} | |
const result = []; | |
for (; start <= end; start += step) { | |
result.push(start); | |
} | |
return result; | |
} | |