Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add [Gen 9] Draft Factory #10213

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions config/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2494,6 +2494,22 @@ export const Formats: import('../sim/dex-formats').FormatList = [
}
},
},
{
name: "[Gen 9] Draft Factory",
desc: `Replay a random matchup from Smogon's Draft League tournaments.`,
team: 'draft',
ruleset: ['Obtainable', 'Species Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview', 'Sleep Clause Mod', 'Endless Battle Clause'],
onBegin() {
for (const [i, side] of this.sides.entries()) {
// Order of team is not changed from the data doc
for (const [j, set] of this.teamGenerator.matchup[i].entries()) {
if (!set.teraCaptain) {
side.pokemon[j].canTerastallize = false;
}
}
}
},
},
{
name: "[Gen 8] Random Battle",
desc: `Randomized teams of level-balanced Pokémon with sets that are generated to be competitively viable.`,
Expand Down
68 changes: 68 additions & 0 deletions data/draft-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {PRNG} from "../sim/prng";
import {deepClone} from "../lib/utils";

interface DraftPokemonSet extends Partial<PokemonSet> {
teraCaptain?: boolean;
}

const sampleData: [DraftPokemonSet[], DraftPokemonSet[]][] = [
[
[
{
name: 'Fred',
species: 'Furret',
item: 'Choice Scarf',
ability: 'Frisk',
moves: ['trick', 'doubleedge', 'knockoff', 'uturn'],
nature: 'Jolly',
evs: {hp: 8, atk: 252, def: 0, spa: 0, spd: 0, spe: 252},
teraCaptain: true,
teraType: 'Normal',
},
],
[
{
species: 'Ampharos',
item: 'Choice Specs',
ability: 'Static',
moves: ['dazzlinggleam', 'thunderbolt', 'focusblast', 'voltswitch'],
nature: 'Modest',
evs: {hp: 248, atk: 0, def: 8, spa: 252, spd: 0, spe: 0},
},
],
],
];

export default class DraftFactory {
dex: ModdedDex;
format: Format;
prng: PRNG;
matchup?: [DraftPokemonSet[], DraftPokemonSet[]];
playerIndex: number;
swapTeams: boolean;
constructor(format: Format | string, seed: PRNG | PRNGSeed | null) {
this.dex = Dex.forFormat(format);
this.format = Dex.formats.get(format);
this.prng = seed instanceof PRNG ? seed : new PRNG(seed);
this.playerIndex = 0;
this.swapTeams = this.prng.randomChance(1, 2);
}

setSeed(seed: PRNGSeed) {
this.prng.seed = seed;
}

getTeam(options?: PlayerOptions | null): PokemonSet[] {
if (this.playerIndex > 1) throw new Error("Can't generate more than 2 teams");

if (!this.matchup) {
this.matchup = deepClone(sampleData[this.prng.next(sampleData.length)]);
if (this.swapTeams) this.matchup!.push(this.matchup!.shift()!);
}

const team: PokemonSet[] = this.matchup![this.playerIndex] as PokemonSet[];

this.playerIndex++;
return team;
}
}
17 changes: 17 additions & 0 deletions databases/schemas/draft-factory.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS draftfactory_sources (
id TEXT PRIMARY KEY,
source_url TEXT NOT NULL UNIQUE
);

CREATE TABLE IF NOT EXISTS gen9draftfactory (
source TEXT NOT NULL,
url1 TEXT NOT NULL,
team1 TEXT NOT NULL,
url2 TEXT NOT NULL,
team2 TEXT NOT NULL,
FOREIGN KEY (source) REFERENCES draftfactory_sources(id) ON DELETE CASCADE,
UNIQUE (url1, url2)
);

DROP VIEW IF EXISTS draftfactory_sources_count;
CREATE VIEW draftfactory_sources_count AS SELECT source, COUNT(*) AS count FROM gen9draftfactory GROUP BY source;
162 changes: 162 additions & 0 deletions server/chat-plugins/draft-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* Tools for managing the Draft Factory database.
*

Check failure on line 3 in server/chat-plugins/draft-factory.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Trailing spaces not allowed
* @author MathyFurret
*/

import {SQL, Net} from "../../lib";
import {Teams} from "../../sim";

interface DraftPokemonSet extends Partial<PokemonSet> {
teraCaptain?: boolean;
}

/**
* Given a PokePaste URL, outputs the URL to access the raw text.
*

Check failure on line 16 in server/chat-plugins/draft-factory.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Trailing spaces not allowed
* This assumes the URL given is either a normal PokePaste URL or already a URL to the raw text.
*/
function getRawURL(url: string): string {
if (url.endsWith('/raw')) return url;
return url + '/raw';
}

/**
* Given a Showdown team export, prepares a stringified JSON of the team.
* Handles extensions used in Draft, such as marking Tera Captains.
*/
function prepareTeamJSON(paste: string): string {
const sets: DraftPokemonSet[] | null = Teams.import(paste);
if (!sets) throw new Error("Could not parse paste");
for (const set of sets) {
if (set.name === "Tera Captain") {
set.name = '';
set.teraCaptain = true;
}
}
return JSON.stringify(sets);
}

class DraftFactoryDB {
db?: SQL.DatabaseManager;
ready: boolean;

constructor() {
this.ready = false;
if (!Config.usesqlite) return;
this.db = SQL(module, {
file: './databases/draft-factory.db',
});
void this.setupDatabase();
}

async setupDatabase() {
if (!this.db) return;
await this.db.runFile('./databases/schemas/draft-factory.sql');
this.ready = true;
}

async getSourcesCount(): Promise<{source: string, count: number}[]> {
if (!this.db || !this.ready) return [];
return this.db.all(`SELECT * FROM draftfactory_sources_count`);
}

/**
* Loads an external CSV file containing PokePaste URLs and adds teams to the database.
* Associates the teams with a source named `sourceName`.
*

Check failure on line 67 in server/chat-plugins/draft-factory.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Trailing spaces not allowed
* A line in the wrong format will cause a rollback.
* An HTTP error or team parsing error will simply skip the line.
*/
async loadCSV(url: string, sourceName: ID): Promise<Error[]> {
if (!this.db || !this.ready) throw new Chat.ErrorMessage("Can't load teams; the DB isn't ready");
await this.db.run(`BEGIN`);
try {
await this.db.run(`INSERT INTO draftfactory_sources (id, source_url) VALUES (?, ?)`, [sourceName, url]);
const insertStatement = await this.db.prepare(`INSERT OR ABORT INTO gen9draftfactory (source, url1, team1, url2, team2) VALUES (?, ?, ?, ?, ?)`);

Check warning on line 76 in server/chat-plugins/draft-factory.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

This line has a length of 145. Maximum allowed is 120
if (!insertStatement) throw new Chat.ErrorMessage("Couldn't parse the insert statement");
const stream = Net(url).getStream();
let line: string | null;
let skipHeader = true;
const pendingAdds = [];
const errors: Error[] = [];
while ((line = await stream.readLine()) !== null) {
if (skipHeader) {
skipHeader = false;
continue;
}
if (!line.trim()) continue;
// player1 name, team url, pokemon, pokemon, player2 name, team url, pokemon, pokemon
const [, url1, , , , url2] = line.split(',');
if (!url1 || !url2) throw new Chat.ErrorMessage("Unexpected format");
const requests = [Net(getRawURL(url1)).get(), Net(getRawURL(url2)).get()];
// pendingRequests.push(...requests);
pendingAdds.push((async () => {
try {
let [paste1, paste2] = await Promise.all(requests);
paste1 = prepareTeamJSON(paste1);
paste2 = prepareTeamJSON(paste2);
await insertStatement.run([sourceName, url1, paste1, url2, paste2]);
} catch (e: any) {
errors.push(e);
}
})());
}
await Promise.all(pendingAdds);
await this.db.run(`COMMIT`);
return errors;
} catch (e) {
await this.db.run(`ROLLBACK`);
throw e;
}
}

async deleteSource(sourceName: ID) {
if (!this.db || !this.ready) throw new Error("The DB isn't ready");
await this.db.run(`DELETE FROM draftfactory_sources WHERE id = ?`, [sourceName]);
}
}

async function setupDatabase(database: SQL.DatabaseManager) {

Check warning on line 120 in server/chat-plugins/draft-factory.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'setupDatabase' is defined but never used
await database.runFile('./databases/schemas/draft-factory.sql');
}

const db: DraftFactoryDB | null = Config.usesqlite ? new DraftFactoryDB() : null;

const WHITELIST = ['mathy'];

function check(ctx: Chat.CommandContext) {
if (!WHITELIST.includes(ctx.user.id)) ctx.checkCan('rangeban');
if (!db) throw new Chat.ErrorMessage(`This feature is not supported because SQLite is disabled.`);
}

export const commands: Chat.ChatCommands = {
draftfactory: {
async import(target) {
check(this);
const args = target.split(',');
if (args.length !== 2) throw new Chat.ErrorMessage(`This command takes exactly 2 arguments.`);
const url = args[0].trim();
const label = toID(args[1]);
const errors = await db!.loadCSV(url, label);
if (errors.length) {
this.errorReply(`Encountered ${errors.length} ${Chat.plural(errors, 'error')}; other sets imported successfully.`);
} else {
this.sendReply(`All sets imported successfully.`);
}
},
importhelp: [
`/draftfactory import url, label - Imports a CSV of Draft Factory teams from the given URL.`,
`The teams will be associated with a label (must be unique); you can delete teams from this label with /draftfactory delete.`,
],
async delete(target) {

Check failure on line 152 in server/chat-plugins/draft-factory.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Async method 'delete' has no 'await' expression
check(this);
db!.deleteSource(toID(target));

Check failure on line 154 in server/chat-plugins/draft-factory.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
},
},
draftfactoryhelp: [
`/draftfactory import url, label - Imports a CSV of Draft Factory teams from the given URL.`,
`/draftfactory delete label - Deletes all teams under the name "label"`,
`Requires: &`,
],
};
2 changes: 2 additions & 0 deletions sim/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,8 @@ export const Teams = new class Teams {
format = Dex.formats.get(format);
if (toID(format).includes('gen9computergeneratedteams')) {
TeamGenerator = require(Dex.forFormat(format).dataDir + '/cg-teams').default;
} else if (toID(format).includes('gen9draftfactory')) {
TeamGenerator = require(Dex.forFormat(format).dataDir + '/draft-factory').default;
} else if (toID(format).includes('gen9superstaffbrosultimate')) {
TeamGenerator = require(`../data/mods/gen9ssb/random-teams`).default;
} else if (toID(format).includes('gen9babyrandombattle')) {
Expand Down
54 changes: 54 additions & 0 deletions test/random-battles/draft-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use strict';

const assert = require('../assert');
const common = require('../common');
const DraftFactory = require('../../dist/data/draft-factory').default;

let battle;

describe('Draft Factory', () => {
afterEach(() => battle.destroy());

it('should only allow the designated Tera Captains to Terastallize', () => {
battle = common.createBattle({formatid: 'gen9draftfactory'});
// Manually create a team generator instance and rig it with data
battle.teamGenerator = new DraftFactory(battle.format, null);
battle.teamGenerator.swapTeams = false;
battle.teamGenerator.matchup = [
[
{
species: 'Furret',
ability: 'keeneye',
moves: ['sleeptalk'],
teraCaptain: true,
teraType: 'Normal',
},
{
species: 'Ampharos',
ability: 'static',
moves: ['sleeptalk'],
},
],
[
{
species: 'Nincada',
ability: 'compoundeyes',
moves: ['sleeptalk'],
},
{
species: 'Marshtomp',
ability: 'torrent',
moves: ['sleeptalk'],
teraCaptain: true,
teraType: 'Fighting',
},
],
];
battle.setPlayer('p1', {});
battle.setPlayer('p2', {});
battle.makeChoices(); // team preview
assert.throws(() => { battle.choose('p2', 'move 1 terastallize'); }, `${battle.p2.pokemon[0].name} should not be able to tera`);
battle.makeChoices('move 1 terastallize', 'switch 2');
battle.makeChoices('auto', 'move 1 terastallize');
});
});
Loading