Last active
May 6, 2021 13:54
-
-
Save MathyFurret/4229a28f419f4aa10fcb2eba9fdd57e9 to your computer and use it in GitHub Desktop.
DefianceChan: A bruteforce Defiance analyzer for Twitch Plays PBR
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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