Skip to content

Commit

Permalink
CGT: Add command to view winrates + misc. updates
Browse files Browse the repository at this point in the history
  • Loading branch information
pyuk-bot committed Feb 16, 2025
1 parent 3df58d2 commit 3d2973c
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 21 deletions.
3 changes: 2 additions & 1 deletion data/cg-team-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export const MOVE_PAIRINGS: {[moveID: IDEntry]: IDEntry} = {

// Bonuses to move ratings by ability
export const ABILITY_MOVE_BONUSES: {[abilityID: IDEntry]: {[moveID: IDEntry]: number}} = {
drought: {sunnyday: 0.2, solarbeam: 2},
contrary: {terablast: 2},
drought: {sunnyday: 0.2, solarbeam: 2},
drizzle: {raindance: 0.2, solarbeam: 0.2, hurricane: 2},
};
// Bonuses to move ratings by move type
export const ABILITY_MOVE_TYPE_BONUSES: {[abilityID: IDEntry]: {[typeName: string]: number}} = {
Expand Down
56 changes: 37 additions & 19 deletions data/cg-teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* - Tracking type coverage to make it more likely that a moveset can hit every type
*/

import {SQLDatabaseManager} from '../lib/sql';
import {Dex, PRNG, SQL} from '../sim';
import {EventMethods} from '../sim/dex-conditions';
import {
Expand Down Expand Up @@ -94,7 +95,9 @@ async function updateLevels(database: SQL.DatabaseManager) {
const updateHistory = await database.prepare(
`INSERT INTO gen9_historical_levels (level, species_id, timestamp) VALUES (?, ?, ${Date.now()})`
);
const data = await database.all('SELECT species_id, wins, losses, level FROM gen9computergeneratedteams');
const data: {species_id: ID, wins: number, losses: number, level: number}[] = await database.all(
'SELECT species_id, wins, losses, level FROM gen9computergeneratedteams'
);
for (let {species_id, wins, losses, level} of data) {
const total = wins + losses;

Expand All @@ -110,12 +113,13 @@ async function updateLevels(database: SQL.DatabaseManager) {
}
}

export let cgtDatabase: SQLDatabaseManager;
if (global.Config && Config.usesqlite && Config.usesqliteleveling) {
const database = SQL(module, {file: './databases/battlestats.db'});
cgtDatabase = SQL(module, {file: './databases/battlestats.db'});

// update every 2 hours
void updateLevels(database);
levelUpdateInterval = setInterval(() => void updateLevels(database), 1000 * 60 * 60 * 2);
void updateLevels(cgtDatabase);
levelUpdateInterval = setInterval(() => void updateLevels(cgtDatabase), 1000 * 60 * 60 * 2);
}

export default class TeamGenerator {
Expand Down Expand Up @@ -296,7 +300,7 @@ export default class TeamGenerator {
movesStats.nonStatusMoves++;
const bp = +move.basePower;
const moveType = TeamGenerator.moveType(move, species);
if (movesStats.attackTypes[moveType] < bp) movesStats.attackTypes[moveType] = bp;
if ((movesStats.attackTypes[moveType] || 0) < bp) movesStats.attackTypes[moveType] = bp;
}

if (!isRound2 && moves.length === 3) {
Expand Down Expand Up @@ -356,15 +360,18 @@ export default class TeamGenerator {
spe: 31,
};

// For Tera Type, we just pick a random type if it's got Tera Blast, Revelation Dance, or no attacking moves,
// and the type of one of its attacking moves otherwise (so it can take advantage of the boosts).
// For Tera Type, we just pick a random type if it's got Tera Blast, Revelation Dance, or no attacking moves
// In the latter case, we avoid picking a type the Pokemon already is, and in the other two we avoid picking a
// type that matches the Pokemon's other moves
// Otherwise, we pick the type of one of its attacking moves
// Pokemon with 3 or more attack types and Pokemon with both Tera Blast and Contrary can also get Stellar type
// but Pokemon with Adaptability never get Stellar because Tera Stellar makes Adaptability have no effect
// Ogerpon's formes are forced to the Tera type that matches their forme
// Terapagos is forced to Stellar type
// Pokemon with Black Sludge don't generally want to tera to a type other than Poison
const hasTeraBlast = moves.some(m => m.id === 'terablast');
const hasRevelationDance = moves.some(m => m.id === 'revelationdance');
let attackingTypes = nonStatusMoves.map(m => TeamGenerator.moveType(this.dex.moves.get(m), species));
let teraType;
if (species.forceTeraType) {
teraType = species.forceTeraType;
Expand All @@ -373,15 +380,15 @@ export default class TeamGenerator {
} else if (hasTeraBlast && ability === 'Contrary' && this.prng.randomChance(2, 3)) {
teraType = 'Stellar';
} else {
let types = nonStatusMoves.map(m => TeamGenerator.moveType(this.dex.moves.get(m), species));
const noStellar = ability === 'Adaptability' || new Set(types).size < 3;
if (hasTeraBlast || hasRevelationDance || !nonStatusMoves.length) {
types = [...this.dex.types.names()];
if (noStellar) types.splice(types.indexOf('Stellar'));
const noStellar = ability === 'Adaptability' || new Set(attackingTypes).size < 3;
const noAttacks = !nonStatusMoves.length;
if (hasTeraBlast || hasRevelationDance || noAttacks) {
attackingTypes = [...this.dex.types.names().filter(t => !(noAttacks ? species.types : attackingTypes).includes(t))];
if (noStellar) attackingTypes.splice(attackingTypes.indexOf('Stellar'));
} else {
if (!noStellar) types.push('Stellar');
if (!noStellar) attackingTypes.push('Stellar');
}
teraType = this.prng.sample(types);
teraType = this.prng.sample(attackingTypes);
}

return {
Expand Down Expand Up @@ -638,11 +645,21 @@ export default class TeamGenerator {
}
if (move.category === 'Special') powerEstimate *= Math.max(0.5, 1 + specialSetup) / Math.max(0.5, 1 + physicalSetup);

const abilityBonus = (
let abilityBonus = (
((ABILITY_MOVE_BONUSES[this.dex.toID(ability)] || {})[move.id] || 1) *
((ABILITY_MOVE_TYPE_BONUSES[this.dex.toID(ability)] || {})[moveType] || 1)
);

const misslePrimers = ['surf', 'dive'];
if (ability === 'Gulp Missle' && misslePrimers.includes(move.id)) {
// we want exactly one move that activates gulp missile
if (!movesSoFar.find(m => m.id === (misslePrimers.filter(p => p !== move.id)[0]))) {
abilityBonus = 3;
} else {
abilityBonus = 0.75;
}
}

let weight = powerEstimate * abilityBonus;
if (move.id in HARDCODED_MOVE_WEIGHTS) weight *= HARDCODED_MOVE_WEIGHTS[move.id];
// semi-hardcoded move weights that depend on having control over the item
Expand All @@ -667,7 +684,7 @@ export default class TeamGenerator {
if (ability === 'Poison Touch') weight *= TeamGenerator.statusWeight('psn', 1 - Math.pow(0.7, numberOfHits));
}
if (move.flags.bite && ability === 'Strong Jaw') weight *= 1.5;
// 5% boost for ability to break subs
// 5% boost for ability to bypass subs
if (move.flags.bypasssub) weight *= 1.05;
if (move.flags.pulse && ability === 'Mega Launcher') weight *= 1.5;
if (move.flags.punch && ability === 'Iron Fist') weight *= 1.2;
Expand Down Expand Up @@ -709,7 +726,7 @@ export default class TeamGenerator {
if (move.self?.volatileStatus) weight *= 0.8;

// downweight moves if we already have an attacking move of the same type
if ((movesStats.attackTypes[moveType] || 0) > 60) weight *= 0.3;
if ((movesStats.attackTypes[moveType] || 0) > 60) weight *= 0.5;

if (move.selfdestruct) weight *= 0.3;
if (move.recoil && ability !== 'Rock Head' && ability !== 'Magic Guard') {
Expand Down Expand Up @@ -919,11 +936,11 @@ export default class TeamGenerator {
switch (item.id) {
// Choice Items
case 'choiceband':
return moves.every(x => TeamGenerator.moveIsPhysical(x, species)) ? 50 : 0;
return moves.every(x => TeamGenerator.moveIsPhysical(x, species) && x.priority < 3) ? 50 : 0;
case 'choicespecs':
return moves.every(x => TeamGenerator.moveIsSpecial(x, species)) ? 50 : 0;
case 'choicescarf':
if (moves.some(x => x.category === 'Status' || x.secondary?.self?.boosts?.spe)) return 0;
if (moves.some(x => x.category === 'Status' || x.secondary?.self?.boosts?.spe || x.priority > 1)) return 0;
if (adjustedStats.spe > 50 && adjustedStats.spe < 120) return 50;
return 10;

Expand All @@ -937,6 +954,7 @@ export default class TeamGenerator {
return 10;
case 'heavydutyboots':
switch (this.dex.getEffectiveness('Rock', species)) {
case 2: return 50; // double super effective
case 1: return 30; // super effective
case 0: return 10; // neutral
}
Expand Down
96 changes: 95 additions & 1 deletion server/chat-plugins/cg-teams-leveling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* Handles updating the level database for [Gen 9] Computer-Generated Teams.
*/

import {SQL} from "../../lib";
import {SQL, Utils} from "../../lib";
import {getSpeciesName} from "./randombattles/winrates";
import {cgtDatabase} from "../../data/cg-teams";

export let addPokemon: SQL.Statement | null = null;
export let incrementWins: SQL.Statement | null = null;
Expand Down Expand Up @@ -68,3 +69,96 @@ export const handlers: Chat.Handlers = {
void updateStats(battle, winner);
},
};

export const commands: Chat.ChatCommands = {
cgtwr: 'cgtwinrates',
cgtwinrates(target, room, user) {
return this.parse(`/j view-cgtwinrates-${target ? 'history--' + target : 'current'}`);
},
cgtwinrateshelp: [
'/cgtwinrates OR /cgtwr - Get a list of the current win rate data for all Pokemon in [Gen 9] Computer Generated Teams.',
],

// Add maintenance commands here
};

interface MonCurrent {species_id: ID; wins: number; losses: number; level: number}
interface MonHistory {level: number; species_id: ID; timestamp: number}

export const pages: Chat.PageTable = {
async cgtwinrates(query, user) {
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
if (!cgtDatabase) {
return this.errorReply(`CGT win rates are not being tracked due to the server's SQL settings.`);
}
query = query.join('-').split('--');
const mode = query.shift();
if (mode === 'current') {
let buf = `<div class="pad"><h2>Winrates for [Gen 9] Computer Generated Teams</h2>`;
const sorter = toID(query.shift() || 'alphabetical');
if (!['alphabetical', 'level'].includes(sorter)) {
return this.errorReply(`Invalid sorting method. Must be either 'alphabetical' or 'level'.`);
}
const otherSort = sorter === 'alphabetical' ? 'Level' : 'Alphabetical';
buf += `<a class="button" target="replace" href="/view-cgtwinrates-current--${toID(otherSort)}">`;
buf += `Sort by ${otherSort} descending</a>`;
buf += `<hr />`;
const statData: MonCurrent[] = await cgtDatabase.all(
'SELECT species_id, wins, losses, level FROM gen9computergeneratedteams'
);
this.title = `[Winrates] [Gen 9] Computer Generated Teams`;
let sortFn: (val: MonCurrent) => Utils.Comparable;

if (sorter === 'alphabetical') {
sortFn = (data) => [data.species_id];
} else {
sortFn = (data) => [-data.level];
}
const mons = Utils.sortBy(statData, sortFn);
buf += `<div class="ladder pad"><table><tr><th>Pokemon</th><th>Level</th><th>Wins</th><th>Losses</th>`;
for (const mon of mons) {
buf += `<tr><td>${Dex.species.get(mon.species_id).name}</td>`;
buf += `<td>${mon.level}</td><td>${mon.wins}</td><td>${mon.losses}</td></tr>`;
}
buf += `</table></div></div>`;
return buf;
} else if (mode === 'history') {
// Restricted because this is a potentially very slow command
this.checkCan('modlog', null, Rooms.get('development')!); // stinky non-null assertion

let speciesID = query.shift();
let buf;
if (speciesID) {
speciesID = getLevelSpeciesID({species: query.shift() || ''} as PokemonSet);
const species = Dex.species.get(speciesID);
if (!species.exists ||
species.isNonstandard || species.isNonstandard === 'Unobtainable' ||
species.nfe ||
species.battleOnly && (!species.requiredItems?.length || species.name.endsWith('-Tera'))
) {
this.errorReply('Species has no data in [Gen 9] Computer Generated Teams');
}
buf = `<div class="pad"><h2>Level history for ${species.name} in [Gen 9] CGT</h2>`;
} else {
buf = `<div class="pad"><h2>Level history for [Gen 9] Computer Generated Teams</h2>`;
}
const history: MonHistory[] = await cgtDatabase.all(
'SELECT level, species_id, timestamp FROM gen9_historical_levels'
);
this.title = `[History] [Gen 9] Computer Generated Teams`;

const MAX_LINES = 100;
let lines = 0;
buf += `<div class="ladder pad"><table><tr><th>Pokemon</th><th>Level</th><th>Timestamp</th>`;
for (const entry of history) {
if (speciesID && entry.species_id !== speciesID) continue;
buf += `<tr><td>${entry.species_id}</td><td>${entry.level}</td>`;
const timestamp = new Date(entry.timestamp);
buf += `<td>${timestamp.toLocaleDateString()}, ${timestamp.toLocaleTimeString()}</td></tr>`;
if (++lines >= MAX_LINES) break;
}
buf += `</table></div></div>`;
return buf;
}
},
};

0 comments on commit 3d2973c

Please sign in to comment.