Skip to content

Commit 2ad39d1

Browse files
SirzBenjieDayKevBlitz425
authored
balance(ai): Moveset generation rebalancing (#6962)
* [WIP] movegen rebalance part 1 * Add script to generate random movesets * finalize movegen improvements * Cleanup licenses in scripts * Add tm denylist * Add level based denylist * Misc cleanup * Allow specifying ability for movegen * Allow specifying form index and egg moves * fix improper conditions for singles moves and level based denylist * Add final filtering for useless moves Also update forced signature moves * Add explicit undefined to allowable payload types * Remove `src/test.ts` * Fix license snippet in gen-moveset-driver * Enable rival-based signature moves * Adjust forbidden-moves Disallowed in singles: +Heal Pulse Disallowed TMs: +Take Down Disallowed Level-Based: +Teleport, +Gear Up, +Imprison, +Spark, +Dream Eater, +Nightmare * Remove Rest from its own superceded list * Sort `forbidden-moves.ts` arrays and add missing TSDoc type import * Fix function name, fix a couple of typos * Format `moveset-generation.ts` * Format `forbidden-moves.ts` and `signature-moves.ts` * Format `superceded-moves.ts` * Fix some docs/comments * Remove duplicate comment * Fix variable name * Fix inconsistencies in superceded moves * Clarify some variable names/comments wrt weather/terrain setting moves * Balance Team adjustments to Sig List * Remove duplicate entry in `signature-moves.ts` * Add new files to `biome.jsonc` so they can be formatted * misc: address comments from code review * fix: pnpx changed to pnpm dlx for cross compat with windows * Update signature-moves.ts * Eradicate duplicate Togedemaru entry * Update forbidden-moves.ts * Fix duplicate comment * fix: gen-moveset script respects custom reporter * feat: add take down to level-based denylist * misc: remove params variable from gen-moveset.ts * docs: add description of forRival param to generateAndPopulateMoveset * fix: actually pass forRival when generating movesets for rivals * feat: Make parental bond impact moveset generation * feat: Make power ceiling cap configurable * fix: removeSelfStatBoost works properly now * Update Forbidden Move and Signature Move List, change Rival Pidgeot ability * Update signature-moves.ts * Add Steel Beam, Psywave, SonicBoom, and Synchronoise to denylist * docs: add docs for `useRivalSignature` to `forceRivalBirdAbility` * Address Kev's comments from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Address Bertie's comment --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Blitzy <118096277+Blitz425@users.noreply.github.com>
1 parent 1678027 commit 2ad39d1

35 files changed

+2839
-194
lines changed

biome.jsonc

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@
1616
"enabled": true,
1717
"useEditorconfig": true,
1818
"indentStyle": "space",
19-
"includes": ["**", "!src/data/balance/**", "src/data/balance/biomes/**", "src/data/balance/timed-events.ts"], // TODO: enable formatting of balance folder
19+
"includes": [
20+
"**",
21+
"!src/data/balance/**",
22+
"src/data/balance/moves/forbidden-moves.ts",
23+
"src/data/balance/moves/signature-moves.ts",
24+
"src/data/balance/moves/superceded-moves.ts",
25+
"src/data/balance/biomes/**",
26+
"src/data/balance/timed-events.ts"
27+
], // TODO: enable formatting of balance folder
2028
"lineWidth": 120
2129
},
2230
"files": {
@@ -315,6 +323,22 @@
315323
}
316324
},
317325
"overrides": [
326+
{
327+
"includes": ["**/src/ai/**/*.ts"],
328+
"linter": {
329+
"rules": {
330+
"complexity": {
331+
"noExcessiveCognitiveComplexity": {
332+
"level": "info",
333+
"options": {
334+
// Code for AI behavior can have a slightly higher complexity due to the nature of decision trees and such
335+
"maxAllowedComplexity": 20
336+
}
337+
}
338+
}
339+
}
340+
}
341+
},
318342
{
319343
"includes": ["**/scripts/**/*.js"],
320344
"linter": {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"update-assets": "pnpm update-submodules assets",
4040
"update-assets:remote": "pnpm update-submodules:remote assets",
4141
"update-submodules": "git submodule update --progress --init --recursive --force --depth 1",
42-
"update-submodules:remote": "pnpm update-submodules --remote"
42+
"update-submodules:remote": "pnpm update-submodules --remote",
43+
"sample-movesets": "pnpm dlx vite-node scripts/gen-moveset/gen-moveset-driver.ts"
4344
},
4445
"dependencies": {
4546
"@material/material-color-utilities": "^0.3.0",

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ onlyBuiltDependencies:
55
- esbuild
66
- lefthook
77
- msw
8+
- vite-node
89

910
overrides:
1011
ajv@>=7.0.0-alpha.0 <8.18.0: '>=8.18.0'
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Pagefault Games
3+
* SPDX-FileContributor: SirzBenjie
4+
*
5+
* SPDX-License-Identifier: AGPL-3.0-only
6+
*/
7+
import { EGG_MOVE_LEVEL_REQUIREMENT } from "#balance/moves/moveset-generation";
8+
import { SpeciesId } from "#enums/species-id";
9+
import { spawn } from "node:child_process";
10+
import net from "node:net";
11+
import { confirm, input, number as promptNumber } from "@inquirer/prompts";
12+
import chalk from "chalk";
13+
import type { SamplerPayload } from "./types";
14+
15+
/**
16+
* Starts a TCP server and returns a Promise that resolves with the dataPromise and port.
17+
*/
18+
function spawnServer(payload: SamplerPayload): Promise<{ dataPromise: Promise<string>; port: number }> {
19+
const { promise: dataPromise, resolve: dataResolver } = Promise.withResolvers<string>();
20+
21+
const server = net.createServer(socket => {
22+
socket.write(JSON.stringify(payload));
23+
let data = "";
24+
socket.on("data", chunk => {
25+
data += chunk.toString();
26+
});
27+
socket.on("end", () => {
28+
server.close();
29+
dataResolver(data);
30+
});
31+
});
32+
33+
const { promise, resolve } = Promise.withResolvers<{ dataPromise: Promise<string>; port: number }>();
34+
server.listen(0, () => {
35+
const port = (server.address() as net.AddressInfo).port;
36+
resolve({ dataPromise, port });
37+
});
38+
39+
return promise;
40+
}
41+
42+
async function getDataFromChildProcess(payload: SamplerPayload): Promise<void> {
43+
const { dataPromise, port } = await spawnServer(payload);
44+
45+
const child = spawn("pnpm", ["vitest", "-c", "scripts/gen-moveset/gen-moveset.config.ts"], {
46+
env: {
47+
...process.env,
48+
COMMUNICATION_PORT: port.toString(),
49+
},
50+
stdio: ["inherit", "ignore", "ignore"],
51+
});
52+
53+
child.on("exit", async () => {
54+
const data = await dataPromise;
55+
if (!data) {
56+
console.error("No data received from child process.");
57+
process.exit(1);
58+
}
59+
60+
process.stdout.write(data + "\n");
61+
});
62+
}
63+
64+
async function promptInputs(): Promise<SamplerPayload> {
65+
const speciesInput = await input({
66+
message: "Enter the species ID or name of the Pokémon to generate movesets for:",
67+
validate: value => {
68+
return SpeciesId[value.toUpperCase()] != null;
69+
},
70+
});
71+
72+
let speciesName: string;
73+
let speciesId = Number(speciesInput) as SpeciesId;
74+
let wasNum = false;
75+
if (Number.isNaN(speciesId)) {
76+
speciesName = speciesInput;
77+
speciesId = SpeciesId[speciesInput.toUpperCase()] as SpeciesId;
78+
} else {
79+
speciesName = SpeciesId[speciesId];
80+
wasNum = true;
81+
}
82+
speciesName = speciesName[0].toUpperCase() + speciesName.slice(1).toLowerCase();
83+
if (wasNum) {
84+
console.log(chalk.bold(` Selected species: ${speciesName}`));
85+
}
86+
87+
const boss = await confirm({
88+
message: "Generate as boss (default yes)?",
89+
default: true,
90+
});
91+
92+
const forTrainer = await confirm({
93+
message: "Generate as for a trainer (default yes)?",
94+
default: true,
95+
});
96+
let forRival = false;
97+
if (forTrainer) {
98+
forRival = await confirm({
99+
message: "Generate as a Rival's Pokémon (default no)?",
100+
default: false,
101+
});
102+
}
103+
104+
const level = await promptNumber({
105+
message: "Enter the level of the Pokémon (default 100):",
106+
default: 100,
107+
max: 200,
108+
min: 3,
109+
required: true,
110+
});
111+
112+
let allowEggMoves: boolean | undefined;
113+
if (forTrainer && level >= EGG_MOVE_LEVEL_REQUIREMENT) {
114+
allowEggMoves = await confirm({
115+
message: "Allow egg moves? (default no)?",
116+
default: false,
117+
});
118+
}
119+
const formIndex = await promptNumber({
120+
message: "Enter the form index to generate (if empty or invalid, will default to the base form):",
121+
default: 0,
122+
min: 0,
123+
required: false,
124+
});
125+
126+
const abilityIndex = await promptNumber({
127+
message: "Enter an ability index to force (leave blank to not force any):",
128+
min: 0,
129+
max: 2,
130+
required: false,
131+
});
132+
133+
const trials = await promptNumber({
134+
message: "Enter the number of movesets to generate (default 100):",
135+
default: 100,
136+
min: 1,
137+
required: true,
138+
});
139+
140+
const printWeights = await confirm({
141+
message: "Print move weight details (default no)?",
142+
default: false,
143+
});
144+
145+
return {
146+
speciesId,
147+
boss,
148+
level,
149+
trials,
150+
printWeights,
151+
forTrainer,
152+
abilityIndex,
153+
allowEggMoves,
154+
formIndex,
155+
forRival,
156+
};
157+
}
158+
159+
async function main() {
160+
let payload: SamplerPayload;
161+
try {
162+
payload = await promptInputs();
163+
} catch (err: any) {
164+
// Suppress annoying stack trace on SIGINT
165+
if (err?.message.includes("User force closed the prompt with SIGINT")) {
166+
process.exit(130);
167+
}
168+
throw err;
169+
}
170+
171+
await getDataFromChildProcess(payload);
172+
}
173+
174+
await main();
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024-2025 Pagefault Games
3+
* SPDX-FileContributor: SirzBenjie
4+
*
5+
* SPDX-License-Identifier: AGPL-3.0-only
6+
*/
7+
8+
import { CustomDefaultReporter } from "#test/reporters/custom-default-reporter";
9+
import type { UserConfig } from "vite";
10+
import { defineConfig } from "vitest/config";
11+
import { sharedConfig } from "../../vite.config";
12+
13+
// biome-ignore lint/style/noDefaultExport: required for vitest
14+
export default defineConfig(async config => {
15+
const viteConfig = await sharedConfig(config);
16+
const opts: UserConfig = {
17+
...viteConfig,
18+
test: {
19+
passWithNoTests: false,
20+
env: {
21+
TZ: "UTC",
22+
},
23+
reporters: [new CustomDefaultReporter()],
24+
setupFiles: ["./test/setup/font-face.setup.ts", "./test/setup/vitest.setup.ts"],
25+
includeTaskLocation: true,
26+
environment: "jsdom",
27+
environmentOptions: {
28+
jsdom: {
29+
resources: "usable",
30+
},
31+
},
32+
restoreMocks: true,
33+
watch: false,
34+
name: "gen-moveset",
35+
include: ["./scripts/gen-moveset/gen-moveset.ts"],
36+
},
37+
};
38+
return opts;
39+
});

0 commit comments

Comments
 (0)