Remove inactive ladders

This commit is contained in:
Kris Johnson 2025-08-31 23:57:15 -06:00
parent 5fb1a1dd85
commit 8797d4628c
11 changed files with 9 additions and 1451 deletions

View File

@ -1,8 +1,7 @@
config/formats.ts @KrisXV @Marty-D
data/mods/gen9ssb/ @HoeenCoder @HisuianZoroark @KrisXV
data/random-battles/ @AnnikaCodes @KrisXV @MathyFurret @ACakeWearingAHat @livid-washed @adrivrie
data/random-battles/ @KrisXV @MathyFurret @ACakeWearingAHat @livid-washed @adrivrie
data/text/ @Marty-D
data/cg-team*.ts @KrisXV @pyuk
databases/ @monsanto
lib/sql.ts @mia-pi-git
server/artemis/* @mia-pi-git

View File

@ -327,6 +327,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [
{
name: "[Gen 9] Ubers UU",
mod: 'gen9',
searchShow: false,
ruleset: ['[Gen 9] Ubers'],
banlist: [
// Ubers OU
@ -3175,6 +3176,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [
name: "[Gen 9] BSS Factory",
desc: `Randomized 3v3 Singles featuring Pokémon and movesets popular in Battle Stadium Singles.`,
mod: 'gen9',
searchShow: false,
team: 'randomBSSFactory',
bestOfDefault: true,
ruleset: ['Flat Rules', 'VGC Timer'],
@ -3183,6 +3185,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [
name: "[Gen 9] Draft Factory",
desc: `Randomized matchups sourced from various 6v6 singles draft leagues.`,
mod: 'gen9',
searchShow: false,
team: 'randomDraftFactory',
bestOfDefault: true,
ruleset: ['Standard Draft', '!Team Preview'],
@ -3218,16 +3221,6 @@ export const Formats: import('../sim/dex-formats').FormatList = [
bestOfDefault: true,
ruleset: ['Obtainable', 'Species Clause', 'HP Percentage Mod', 'Cancel Mod', 'Sleep Clause Mod', 'Illusion Level Mod'],
},
{
name: "[Gen 9] Computer-Generated Teams",
desc: `Teams generated automatically based on heuristics (rules), with levels based on previous success/failure in battle. ` +
`Not affiliated with Random Battles formats. Some sets will by nature be worse than others, but you can report egregiously bad sets ` +
`with <a href="https://forms.gle/DYwQN5qGVegz3YU38">this form</a>.`,
mod: 'gen9',
team: 'computerGenerated',
bestOfDefault: true,
ruleset: ['Obtainable', 'Species Clause', 'HP Percentage Mod', 'Cancel Mod', 'Sleep Clause Mod', 'Illusion Level Mod'],
},
{
name: "[Gen 9] Hackmons Cup",
desc: `Randomized teams of level-balanced Pok&eacute;mon with absolutely any ability, moves, and item.`,

View File

@ -62,8 +62,6 @@ export const Aliases: import('../sim/dex').AliasesTable = {
gen6hackmons: "[Gen 6] Pure Hackmons",
cc1v1: "[Gen 9] Challenge Cup 1v1",
cc2v2: "[Gen 9] Challenge Cup 2v2",
cgt: "[Gen 9] Computer-Generated Teams",
compgen: "[Gen 9] Computer-Generated Teams",
hc: "[Gen 9] Hackmons Cup",
bf: "[Gen 8] Battle Factory",
bssf: "[Gen 9] BSS Factory",
@ -90,8 +88,8 @@ export const Aliases: import('../sim/dex').AliasesTable = {
gen6ag: "[Gen 6] Anything Goes",
crossevo: "[Gen 9] Cross Evolution",
mayhem: "[Gen 9] Random Battle Mayhem",
omotm: "[Gen 9] Alphabet Cup",
lcotm: "[Gen 9] Inheritance",
omotm: "[Gen 9] Bad 'n Boosted",
lcotm: "[Gen 9] Revelationmons",
// mega evos --- 1st ordered alphabetically by species, 2nd by alias
megasnow: "Abomasnow-Mega",

View File

@ -1,78 +0,0 @@
// Data for computer-generated teams
export const MOVE_PAIRINGS: { [moveID: IDEntry]: IDEntry } = {
rest: 'sleeptalk',
sleeptalk: 'rest',
};
// Bonuses to move ratings by ability
export const ABILITY_MOVE_BONUSES: { [abilityID: IDEntry]: { [moveID: IDEntry]: number } } = {
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 } } = {
darkaura: { Dark: 1.33 },
dragonsmaw: { Dragon: 1.5 },
fairyaura: { Fairy: 1.33 },
steelworker: { Steel: 1.5 },
steelyspirit: { Steel: 1.5 },
transistor: { Electric: 1.3 },
// -ate moves
pixilate: { Normal: 1.5 * 1.2 },
refrigerate: { Normal: 1.5 * 1.2 },
aerilate: { Normal: 1.5 * 1.2 },
normalize: { Normal: 1.2 },
// weather
drizzle: { Water: 1.4, Fire: 0.6 },
drought: { Fire: 1.4, Water: 0.6 },
};
// For moves whose quality isn't obvious from data
// USE SPARINGLY!
export const HARDCODED_MOVE_WEIGHTS: { [moveID: IDEntry]: number } = {
// Fails unless user is asleep
snore: 0,
// Hard to use
lastresort: 0.1, dreameater: 0.1,
// Useless without Berry + sucks even then
belch: 0.2,
// Power increases in conditions out of our control that may occur
avalanche: 1.2,
ficklebeam: 1.3,
hex: 1.2,
stompingtantrum: 1.2,
temperflare: 1.2,
// Attacks that set hazards on hit
// We REALLY like hazards
stoneaxe: 16,
ceaselessedge: 16,
// screens
lightscreen: 3, reflect: 3, auroraveil: 3, // TODO: make sure AVeil always gets Snow?
tailwind: 2,
// mess with the opponent
taunt: 2, disable: 2, encore: 3,
// healing moves
// TODO: should healing moves be more common on bulkier pokemon?
// 25%
junglehealing: 3, lifedew: 3,
// 50%
milkdrink: 5, moonlight: 5, morningsun: 5, recover: 5, roost: 5,
shoreup: 5, slackoff: 5, softboiled: 5, synthesis: 5,
// delayed/consequence
rest: 3, // has sleeptalk potential
wish: 2,
// requires terrain
steelroller: 0.1,
};
export const WEIGHT_BASED_MOVES = ['heatcrash', 'heavyslam', 'lowkick', 'grassknot'];
export const TARGET_HP_BASED_MOVES = ['crushgrip', 'hardpress'];

File diff suppressed because it is too large Load Diff

View File

@ -2696,7 +2696,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
basePower: 0,
category: "Status",
shortDesc: "Changes target to a Randbats set.",
desc: "Z-Move requiring Irpatuzinium Z. Nearly always moves first. Permanently transforms the target into a randomized Pokemon that would be generated in one of the following formats: Gen 9 Random Battle, Gen 9 Hackmons Cup, Gen 9 Challenge Cup, or Computer-Generated Teams. In the vast majority of circumstances, this also prevents the target from acting this turn.",
desc: "Z-Move requiring Irpatuzinium Z. Nearly always moves first. Permanently transforms the target into a randomized Pokemon that would be generated in one of the following formats: Gen 9 Random Battle, Gen 9 Hackmons Cup, or Gen 9 Challenge Cup. In the vast majority of circumstances, this also prevents the target from acting this turn.",
name: "Bibbidi-Bobbidi-Rands",
gen: 9,
pp: 1,
@ -2706,7 +2706,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
this.attrLastMove('[anim] Doom Desire');
},
onHit(target, source) {
const formats = ['gen9randombattle', 'gen9hackmonscup', 'gen9challengecup1v1', 'gen9computergeneratedteams'];
const formats = ['gen9randombattle', 'gen9hackmonscup', 'gen9challengecup1v1'];
const randFormat = this.sample(formats);
let msg;
switch (randFormat) {
@ -2719,9 +2719,6 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
case 'gen9challengecup1v1':
msg = "The only difference between a Challenge Cup Pokémon and my in-game one is that the former actually surpassed lvl. 60, enjoy n.n";
break;
case 'gen9computergeneratedteams':
msg = "We asked an AI to make a randbats set. YOU WON'T BELIEVE WHAT IT CAME UP WITH N.N";
break;
}
let team = [] as PokemonSet[];
const unModdedDex = Dex.mod('base');

View File

@ -2016,11 +2016,6 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = {
desc: "Maximum team size (number of pokemon) that can be brought into Team Preview (or into the battle, in formats without Team Preview)",
hasValue: 'positive-integer',
// hardcoded in sim/team-validator
onValidateRule(value) {
if (this.format.id.endsWith('computergeneratedteams')) {
throw new Error(`${this.format.name} does not support Max Team Size.`);
}
},
},
maxmovecount: {
effectType: 'ValidatorRule',

View File

@ -1,22 +0,0 @@
CREATE TABLE IF NOT EXISTS gen9computergeneratedteams (
species_id TEXT PRIMARY KEY,
wins NUMBER NOT NULL,
losses NUMBER NOT NULL,
level NUMBER NOT NULL
);
CREATE INDEX IF NOT EXISTS gen9computergeneratedteams_species_id_level ON gen9computergeneratedteams(species_id, level);
CREATE TABLE IF NOT EXISTS gen9_historical_levels (
species_id TEXT NOT NULL,
level NUMBER NOT NULL,
timestamp NUMBER NOT NULL
);
CREATE TABLE IF NOT EXISTS db_info (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT INTO db_info (key, value) VALUES ('version', '1') ON CONFLICT DO NOTHING;
PRAGMA journal_mode=WAL;

View File

@ -1,163 +0,0 @@
/**
* Not a chat plugin.
*
* Handles updating the level database for [Gen 9] Computer-Generated Teams.
*/
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;
export let incrementLosses: SQL.Statement | null = null;
export let dbSetupPromise: Promise<void> | null = null;
async function setupDatabase(database: SQL.DatabaseManager) {
await database.runFile('./databases/schemas/battlestats.sql');
addPokemon = await database.prepare(
'INSERT OR IGNORE INTO gen9computergeneratedteams (species_id, wins, losses, level) VALUES (?, 0, 0, ?)'
);
incrementWins = await database.prepare(
'UPDATE gen9computergeneratedteams SET wins = wins + 1 WHERE species_id = ?'
);
incrementLosses = await database.prepare(
'UPDATE gen9computergeneratedteams SET losses = losses + 1 WHERE species_id = ?'
);
}
if (Config.usesqlite && Config.usesqliteleveling) {
const database = SQL(module, {
file: './databases/battlestats.db',
});
dbSetupPromise = setupDatabase(database);
}
function getLevelSpeciesID(set: PokemonSet, format?: Format) {
if (['Basculin', 'Greninja'].includes(set.name)) return toID(set.species);
return toID(getSpeciesName(set, format || Dex.formats.get('gen9computergeneratedteams')));
}
async function updateStats(battle: RoomBattle, winner: ID) {
if (!incrementWins || !incrementLosses) await dbSetupPromise;
if (toID(battle.format) !== 'gen9computergeneratedteams') return;
// if the game is rated or part of a tournament hosted by a public room, it counts
if (battle.rated <= 1 && battle.room.parent?.game) {
let parent = battle.room.parent;
if (parent.game!.gameid === 'bestof' && parent.parent?.game) parent = parent.parent;
if (parent.game!.gameid !== 'tournament' || parent.settings.isPrivate) return;
} else if (battle.rated < 1000) {
return;
}
for (const player of battle.players) {
const team = await battle.getPlayerTeam(player);
if (!team) return;
const increment = (player.id === winner ? incrementWins : incrementLosses);
for (const set of team) {
const statsSpecies = getLevelSpeciesID(set, Dex.formats.get(battle.format));
await addPokemon?.run([statsSpecies, set.level || 100]);
await increment?.run([statsSpecies]);
}
}
}
export const handlers: Chat.Handlers = {
onBattleEnd(battle, winner) {
if (!Config.usesqlite || !Config.usesqliteleveling) return;
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) {
throw new Chat.ErrorMessage(`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)) {
throw new Chat.ErrorMessage(`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: speciesID || '' } 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;
buf += `<div class="ladder pad"><table><tr><th>Pokemon</th><th>Level</th><th>Timestamp</th>`;
for (let i = history.length - 1; history.length - i <= MAX_LINES; i--) {
const entry = history[i];
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>`;
}
buf += `</table></div></div>`;
return buf;
}
},
};

View File

@ -630,9 +630,7 @@ export const Teams = new class Teams {
let mod = format.mod;
if (format.mod === 'monkeyspaw') mod = 'gen9';
const formatID = toID(format);
if (formatID.includes('gen9computergeneratedteams')) {
TeamGenerator = require(Dex.forFormat(format).dataDir + '/cg-teams').default;
} else if (mod === 'gen9ssb') {
if (mod === 'gen9ssb') {
TeamGenerator = require(`../data/mods/gen9ssb/random-teams`).default;
} else if (formatID.includes('gen9donotuserandombattle')) {
TeamGenerator = require(`../data/random-battles/donotuse/teams`).default;

View File

@ -1,61 +0,0 @@
'use strict';
const assert = require('assert').strict;
const TeamGenerator = require('../../dist/data/cg-teams').default;
describe('[Gen 9] Computer-Generated Teams', () => {
it.skip('should give all species 4 or fewer moves', () => {
const generator = new TeamGenerator();
const pool = generator.dex.species
.all()
.filter(s => s.exists && !(s.isNonstandard || s.isNonstandard === 'Unobtainable') && !s.nfe);
for (const species of pool) {
const set = generator.makeSet(species, { hazardSetters: {} });
assert(set.moves.length <= 4, `Species ${species.name} has more than 4 moves (set=${JSON.stringify(set)})`);
assert(new Set(set.moves).size === set.moves.length, `Species ${species.name} has duplicate moves (set=${JSON.stringify(set)})`);
}
});
// Skipped since it includes randomness; useful for debugging though
it.skip('should have an accurate weighted picker', () => {
const generator = new TeamGenerator();
const numTrials = 100000;
let error = 0;
let trials = 0;
for (const choices of [
[{ choice: 'a', weight: 1 }, { choice: 'b', weight: 2 }],
[{ choice: 'a', weight: 1 }, { choice: 'b', weight: 1 }],
[{ choice: 'a', weight: 30 }, { choice: 'b', weight: 2000 }, { choice: 'c', weight: 7 }],
// a big test case with lots of different weight values
[
{ choice: 'a', weight: 1345 }, { choice: 'b', weight: 2013 }, { choice: 'c', weight: 3411 }, { choice: 'd', weight: 940 },
{ choice: 'e', weight: 505 }, { choice: 'f', weight: 10148 }, { choice: 'g', weight: 7342 }, { choice: 'h', weight: 8403 },
{ choice: 'i', weight: 9859 }, { choice: 'j', weight: 1042 }, { choice: 'k', weight: 1132 }, { choice: 'l', weight: 1200 },
],
]) {
const results = {};
for (let i = 0; i < numTrials; i++) {
const res = generator.weightedRandomPick(choices.map(x => x.choice), c => choices.find(x => x.choice === c)?.weight || 0);
// console.log(`"${res}"`);
if (!results[res]) results[res] = 0;
results[res]++;
}
let totalWeight = 0;
for (const choice of choices) {
totalWeight += choice.weight;
}
for (const [choice, count] of Object.entries(results)) {
const c = choices.find(x => x.choice === choice);
const expected = (c.weight / totalWeight) * numTrials;
error += Math.abs(count - expected) / expected;
trials++;
}
}
const percentError = (error / trials) * 100;
assert(percentError < 3, `Weighted picker error is too high: ${percentError.toFixed(1)}%`);
});
});