Skip to content

Instantly share code, notes, and snippets.

@MathyFurret
Last active May 6, 2021 13:54
Show Gist options
  • Save MathyFurret/4229a28f419f4aa10fcb2eba9fdd57e9 to your computer and use it in GitHub Desktop.
Save MathyFurret/4229a28f419f4aa10fcb2eba9fdd57e9 to your computer and use it in GitHub Desktop.
DefianceChan: A bruteforce Defiance analyzer for Twitch Plays PBR
/*
Version 0.2 updated Nov 25, 2018.
This script will bruteforce a live match from the TPP API
in Defiance mode and estimate winrates.
Requirements:
-An oauth for the TPP API: https://twitchplayspokemon.tv/show_oauth_token
-Node.js: https://nodejs.org/en/download/
-sync-request for Node.js: `npm install -g sync-request` (NPM should come installed with Node.js)
-Pokemon Showdown: https://github.com/Zarel/Pokemon-Showdown
You don't need to install its dependencies, the simulator itself does not require them.
-My PBR mod for Pokemon Showdown: https://github.com/mathfreak231/random-showdown-mods
This is required to accurately simulate PBR. Note that it is still in development.
Setup steps:
Copy the pbr directory from random-showdown-mods into Pokemon-Showdown/mods.
Fill in the relative path to Pokemon-Showdown/sim below
Fill in your TPP API oauth token below
Change the number constants if you wish
Then just `node defiancechan.js`, sit back, and enjoy your simulation!
Note that the PBR mod is *not* entirely accurate. Known inaccuracies include,
but are not limited to: draws are not handled properly, and Fury Cutter
still resets when it shouldn't.
*/
// Replace this with a relative path to Pokemon-Showdown/sim
const Sim = require('./../Pokemon-Showdown/sim');
// Replace this with your TPP oauth token
const TPP_OAUTH = "abc123xyz69420";
// z* for your desired confidence level. This is used to calculate
// the margin of sampling error. Some common values:
// For 95% confidence: 1.959963986
// For 99% confidence: 2.575829303
// In practice this means that C% of your simulations will
// have the true proportion within the margin of error from
// the sampled proportion.
// If you don't know what any of this means, don't bother changing
// or paying attention to the "Margin of error" output at the end.
const Z_STAR = 2.575829303;
// Maximum time to run the simulation for, in milliseconds.
const MAX_TIME = 30000;
// If this is true, it prints some match logs (1 in DEBUG_EVERY) to stdout while it's bashing them.
const DEBUG = false;
const DEBUG_EVERY = 25;
// Set this to require a .json file to bash a match there instead of a live match.
const TEST_MATCH_PATH = null;
/////////////////////////////////////////
// DON'T EDIT ANYTHING BELOW THIS LINE //
/////////////////////////////////////////
const request = require('sync-request');
const PBR_FORMAT = {
id: 'tppbr',
name: 'TPPBR',
mod: 'pbr',
ruleset: ["PBR Sleep Clause", "PBR Freeze Clause", "PBR Self-Destruct Clause", "PBR Destiny Bond Clause"],
banlist: [],
unbanlist: [],
debug: true,
}
/**
* because pokecat TriHard
*/
function convert_stats_table(stats) {
let newstats = {};
for (statname in stats) {
newstats[statname.toLowerCase()] = stats[statname];
}
return newstats;
}
function main() {
console.log("Getting current match...");
let match;
if (TEST_MATCH_PATH) {
match = require(TEST_MATCH_PATH);
} else {
const res = request('GET', "https://twitchplayspokemon.tv/api/current_match", {
headers: {
'OAuth-Token': TPP_OAUTH
},
});
match = JSON.parse(res.getBody('utf8'));
}
console.log("Done.");
if ((match.base_gimmicks.includes('blind_bet') || match.base_gimmicks.includes('secrecy')) && !match.started) {
throw new Error("Can't analyze a match unless all the Pokemon have been revealed.");
}
if (!match.base_gimmicks.includes('defiance')) {
console.log("WARNING: This is not a defiance match!");
}
const startTime = Date.now();
let wincounter = {
Blue: 0,
Red: 0,
};
let drawCount = 0;
let teams = [[], []];
for (let i = 0; i < teams.length; i++) {
for (const pokemon of match.teams[i]) {
teams[i].push({
name: pokemon.ingamename,
species: pokemon.species.name, // TODO: handle forms
item: pokemon.item.name,
ability: pokemon.ability.name,
moves: pokemon.moves.map(move => move.name),
nature: pokemon.nature.name,
gender: pokemon.gender ? pokemon.gender.toUpperCase() : 'N',
evs: convert_stats_table(pokemon.evs),
ivs: convert_stats_table(pokemon.ivs),
level: pokemon.level,
shiny: pokemon.shiny,
happiness: pokemon.happiness,
});
}
}
const isRandomOrder = match.base_gimmicks.includes('random_order');
const isTraitor = match.base_gimmicks.includes('traitor');
const isFog = match.base_gimmicks.includes('fog');
let battle;
let i;
console.log("Begin simulation of matches...");
for (i = 1;; i++) {
battle = new Sim.Battle({
formatid: PBR_FORMAT,
});
// prevent battle from starting so we can edit stuff first
battle.started = true;
let newTeams = teams;
if (!match.started && (isRandomOrder || isTraitor)) {
// TODO: In what order are these gimmicks applied?
newTeams = teams.map(team => team.slice());
if (isRandomOrder) {
for (let team of newTeams) {
battle.shuffle(team);
}
}
if (isTraitor) {
// swap 2 pokes, same position
const p = battle.random(3);
const temp = newTeams[0][p];
newTeams[0][p] = newTeams[1][p];
newTeams[1][p] = temp;
}
}
battle.join('p1', 'Blue', 1, newTeams[0]);
battle.join('p2', 'Red', 2, newTeams[1]);
for (const side of battle.sides) {
for (const pokemon of side.pokemon) {
pokemon.originalPosition = pokemon.position;
for (const [m, move] of pokemon.baseMoveSlots.entries()) {
// change move PP
const moveData = battle.getMove(move.id);
const oldMoveData = match.teams[side.n][pokemon.position].moves[m];
if (oldMoveData) {
move.pp = oldMoveData.pp;
move.maxpp = moveData.pp * (5 + oldMoveData.pp_ups) / 5;
}
}
pokemon.clearVolatile(); // actually update the moveslots TriHard
}
}
if (match.stage) {
battle.colosseum = match.stage;
} else if (isFog && battle.randomChance(1,2)) {
// TODO: confirm this is correct
battle.colosseum = 'courtyard';
} else {
battle.colosseum = battle.sample([
'gateway', 'mainstreet', 'waterfall', 'neon', 'crystal',
'sunnypark', 'magma', 'sunset', 'stargazer', 'lagoon',
]);
}
battle.started = false;
battle.start();
if (isFog) battle.setWeather('fog');
while (!battle.ended) {
if (battle.turn > 500) {
console.log('===BEGIN BAD MATCH LOG EXPORT===');
console.log(battle.log.join('\n'));
console.log('===END BAD MATCH LOG EXPORT===');
console.log('===BEGIN BAD MATCH INPUT LOG EXPORT===');
console.log(battle.inputLog.join('\n'));
console.log('===END BAD MATCH INPUT LOG EXPORT===');
throw new Error("The match fucked up somehow.");
}
for (const side of battle.sides) {
let result;
switch (side.currentRequest) {
case 'switch':
// TPPBR switching rules.
let target = null;
for (let i = side.active.length; i < side.pokemon.length; i++) {
const pokemon = side.pokemon[i];
if (!(pokemon.fainted) && (!target || pokemon.originalPosition < target.originalPosition)) {
target = pokemon;
}
}
result = battle.choose(side.id, `switch ${target.position + 1}`);
if (!result) throw new Error(side.choice.error);
break;
case 'move':
// Same as TPPBR, choose random moves until one works
let tries = 0;
do {
result = battle.choose(side.id, `move ${battle.random(4) + 1}`);
if (++tries > 50) throw new Error(`${side.id} stuck on a move choice: ${side.choice.error}`);
} while (!result);
break;
}
}
}
if (battle.winner) wincounter[battle.winner]++;
else drawCount++;
if (DEBUG && i % DEBUG_EVERY === 0) {
console.log('===BEGIN RANDOM MATCH LOG EXPORT===');
console.log(battle.log.join('\n'));
console.log('===END RANDOM MATCH LOG EXPORT===');
console.log('===BEGIN RANDOM MATCH INPUT LOG EXPORT===');
console.log(battle.inputLog.join('\n'));
console.log('===END RANDOM MATCH INPUT LOG EXPORT===');
}
battle.destroy();
if (MAX_TIME && Date.now() - startTime >= MAX_TIME) {
break;
}
}
console.log(`Simulated ${i} battles in ${Date.now() - startTime}ms.`);
if (drawCount > 0) {
console.log(`Of these, ${drawCount} draw(s) were discarded.`);
i -= drawCount;
}
let favoredTeam = wincounter['Blue'] > wincounter['Red'] ? 'Blue' : 'Red';
let favoredWinrate = wincounter[favoredTeam] / i;
console.log(`${favoredTeam} win chance: ${favoredWinrate}`);
// Large Counts condition
if (wincounter['Blue'] >= 10 && wincounter['Red'] >= 10) {
let standardError = Math.sqrt(favoredWinrate * (1-favoredWinrate) / i);
console.log(`Margin of error: ${Z_STAR * standardError}`);
} else {
console.log("Counts are too small to infer a margin of error.");
}
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment