From d0e3770738149382d8008ef3894679bbb11a03a8 Mon Sep 17 00:00:00 2001 From: Joshua Date: Mon, 20 Oct 2025 20:04:27 -0700 Subject: [PATCH 1/4] Fix profanity filter + new username generator The profanity filter was fixed up to account for cases like [C]rap and C_rap. The filter was extended slightly, and the `RegExpMatcher` ctor was fixed (transformers can't use the same `...` syntax like `englishDataset` and `englishRecommendedTransformers` Additionally, the scattered set of name generators used for players was replaced with one single name generator that comes up with some snazzy stuff like "Space Cow", "Secret Gnome Agenda", and "Comically Large Snail". Copyright: No AI was used in the making of this patch. --- src/client/UsernameInput.ts | 3 +- src/core/Util.ts | 15 +--- src/core/utilities/UsernameGenerator.ts | 104 ++++++++++++++++++++++++ src/core/validations/username.ts | 39 +++++++-- 4 files changed, 140 insertions(+), 21 deletions(-) create mode 100644 src/core/utilities/UsernameGenerator.ts diff --git a/src/client/UsernameInput.ts b/src/client/UsernameInput.ts index 3b3c24b7d5..41f6586736 100644 --- a/src/client/UsernameInput.ts +++ b/src/client/UsernameInput.ts @@ -7,6 +7,7 @@ import { MAX_USERNAME_LENGTH, validateUsername, } from "../core/validations/username"; +import { getRandomUsername } from "../core/utilities/UsernameGenerator"; const usernameKey: string = "username"; @@ -94,7 +95,7 @@ export class UsernameInput extends LitElement { } private generateNewUsername(): string { - const newUsername = "Anon" + this.uuidToThreeDigits(); + const newUsername = getRandomUsername(Math.random()) this.storeUsername(newUsername); return newUsername; } diff --git a/src/core/Util.ts b/src/core/Util.ts index c5cbe044ea..8824d8276e 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -12,10 +12,7 @@ import { Winner, } from "./Schemas"; -import { - BOT_NAME_PREFIXES, - BOT_NAME_SUFFIXES, -} from "./execution/utils/BotNames"; +import { getRandomUsername } from "./utilities/UsernameGenerator"; export function manhattanDistWrapped( c1: Cell, @@ -276,16 +273,10 @@ export function createRandomName( name: string, playerType: string, ): string | null { - let randomName: string | null = null; if (playerType === "HUMAN") { - const hash = simpleHash(name); - const prefixIndex = hash % BOT_NAME_PREFIXES.length; - const suffixIndex = - Math.floor(hash / BOT_NAME_PREFIXES.length) % BOT_NAME_SUFFIXES.length; - - randomName = `πŸ‘€ ${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`; + return getRandomUsername(simpleHash(name)); } - return randomName; + return null; } export const emojiTable = [ diff --git a/src/core/utilities/UsernameGenerator.ts b/src/core/utilities/UsernameGenerator.ts new file mode 100644 index 0000000000..417f87ed9e --- /dev/null +++ b/src/core/utilities/UsernameGenerator.ts @@ -0,0 +1,104 @@ +import { simpleHash } from "../Util" + +const PLURAL_NOUN = Symbol("plural!") +const NOUN = Symbol("noun!") + +const names = [ + ["World Famous", NOUN], + ["Comically Large", NOUN], + ["Comically Small", NOUN], + ["Clearance Aisle", PLURAL_NOUN], + [NOUN, "For Hire"], + ["Suspicious", NOUN], + ["Sopping Wet", PLURAL_NOUN], + ["Smelly", NOUN], + ["Friendly", NOUN], + ["Tardy", NOUN], + ["Evil", NOUN], + [PLURAL_NOUN, "That Bite"], + ["Malicious", NOUN], + ["Spiteful", NOUN], + ["Mister", NOUN], + ["Alternate", NOUN,"Universe"], + [NOUN, "Island"], + [NOUN, "Kingdom"], + [NOUN, "Empire"], + [NOUN, "Dynasty"], + [NOUN, "Cartel"], + [NOUN, "Cabal"], + ["Not Too Fond Of", PLURAL_NOUN], + ["Honk For", PLURAL_NOUN], + ["Canonically Evil", NOUN], + ["Limited Edition", NOUN], + [NOUN, "Scientist"], + ["Famous", NOUN, "Collection"], + ["Supersonic", NOUN, "Spaceship"], + ["Patent Pending", NOUN], + ["Patented", NOUN], + ["Space", NOUN], + ["Secret", NOUN, "Agenda"], + [PLURAL_NOUN, "in my walls"], + ["The", PLURAL_NOUN, "are SPIES"], + ["Traveling", NOUN, "Circus"] +] + +const nouns = [ + "Snail", + "Cow", + "Giraffe", + "Donkey", + "Horse", + "Mushroom", + "Salad", + "Kitten", + "Fork", + "Apple", + "Pancake", + "Tree", + "Fern", + "Seashell", + "Turtle", + "Casserole", + "Gnome", + "Frog", +] + +function isSeedAcceptable(sanitizedSeed: number) { + let template = names[sanitizedSeed % names.length]; + let noun = nouns[Math.floor(sanitizedSeed / names.length) % nouns.length]; + + let totalLength = + template.map((v) => ((v as any)?.length ?? 0)).reduce((a,b) => a + b) + + template.length + + noun.length; + + return totalLength <= 26 +} +/** + * Generate a random username based on a numeric seed + * @param seed - the seed to use to select a username + * @returns a string suitable for a player username + */ +export function getRandomUsername(seed: number) : string { + let sanitizedSeed = Math.floor((seed * 2999) % (names.length * nouns.length)); + let template = names[sanitizedSeed % names.length]; + let noun = nouns[Math.floor(sanitizedSeed / names.length) % nouns.length]; + let result: [string?] = []; + + while (!isSeedAcceptable(sanitizedSeed)) { + sanitizedSeed += 1; + } + + // Convert template to some somewhat-legible word string + for (let step of template) { + if (step == PLURAL_NOUN) { + result.push(`${noun}s`); + } else if (step == NOUN) { + result.push(noun); + } else { + result.push(step.toString()); + } + } + + return result.join(" ") +} \ No newline at end of file diff --git a/src/core/validations/username.ts b/src/core/validations/username.ts index a7fe4f9dd8..75af113c9a 100644 --- a/src/core/validations/username.ts +++ b/src/core/validations/username.ts @@ -1,22 +1,45 @@ import { + DataSet, RegExpMatcher, collapseDuplicatesTransformer, englishDataset, - englishRecommendedTransformers, + pattern, resolveConfusablesTransformer, resolveLeetSpeakTransformer, skipNonAlphabeticTransformer, + toAsciiLowerCaseTransformer, } from "obscenity"; import { translateText } from "../../client/Utils"; import { simpleHash } from "../Util"; +import { getRandomUsername } from "../utilities/UsernameGenerator"; + +const customDataset = new DataSet() + .addAll(englishDataset) + .addPhrase((phrase) => + phrase.setMetadata({ originalWord: 'nigg' }) + /* Not used by any english words */ + .addPattern(pattern`niqq`)) const matcher = new RegExpMatcher({ - ...englishDataset.build(), - ...englishRecommendedTransformers, - ...resolveConfusablesTransformer(), - ...skipNonAlphabeticTransformer(), - ...collapseDuplicatesTransformer(), - ...resolveLeetSpeakTransformer(), + ...customDataset.build(), + + blacklistMatcherTransformers: [ + resolveConfusablesTransformer(), + resolveLeetSpeakTransformer(), + skipNonAlphabeticTransformer(), + toAsciiLowerCaseTransformer(), + collapseDuplicatesTransformer({ + customThresholds: new Map([ + ['b', 2], + ['e', 2], + ['o', 2], + ['l', 2], + ['s', 2], + ['g', 2], + ['q', 2] + ]), + }) + ] }); export const MIN_USERNAME_LENGTH = 3; @@ -36,7 +59,7 @@ const shadowNames = [ export function fixProfaneUsername(username: string): string { if (isProfaneUsername(username)) { - return shadowNames[simpleHash(username) % shadowNames.length]; + return getRandomUsername(simpleHash(username)); } return username; } From c5e534a3336a84a676ad3e2de65bf411f8046b9a Mon Sep 17 00:00:00 2001 From: Joshua Date: Mon, 20 Oct 2025 20:25:38 -0700 Subject: [PATCH 2/4] Tests, Formatting, Lints, & Edge Cases Oops, forgot to finish actually implementing the edge case for when a generated name is too long. Also, formatting, tests, and lints. --- src/client/UsernameInput.ts | 4 +- src/core/utilities/UsernameGenerator.ts | 171 ++++++++++++------------ src/core/validations/username.ts | 42 +++--- tests/Censor.test.ts | 25 ++-- 4 files changed, 117 insertions(+), 125 deletions(-) diff --git a/src/client/UsernameInput.ts b/src/client/UsernameInput.ts index 41f6586736..bc1163e60b 100644 --- a/src/client/UsernameInput.ts +++ b/src/client/UsernameInput.ts @@ -3,11 +3,11 @@ import { customElement, property, state } from "lit/decorators.js"; import { v4 as uuidv4 } from "uuid"; import { translateText } from "../client/Utils"; import { UserSettings } from "../core/game/UserSettings"; +import { getRandomUsername } from "../core/utilities/UsernameGenerator"; import { MAX_USERNAME_LENGTH, validateUsername, } from "../core/validations/username"; -import { getRandomUsername } from "../core/utilities/UsernameGenerator"; const usernameKey: string = "username"; @@ -95,7 +95,7 @@ export class UsernameInput extends LitElement { } private generateNewUsername(): string { - const newUsername = getRandomUsername(Math.random()) + const newUsername = getRandomUsername(Math.random()); this.storeUsername(newUsername); return newUsername; } diff --git a/src/core/utilities/UsernameGenerator.ts b/src/core/utilities/UsernameGenerator.ts index 417f87ed9e..73505c53e6 100644 --- a/src/core/utilities/UsernameGenerator.ts +++ b/src/core/utilities/UsernameGenerator.ts @@ -1,104 +1,103 @@ -import { simpleHash } from "../Util" - -const PLURAL_NOUN = Symbol("plural!") -const NOUN = Symbol("noun!") +const PLURAL_NOUN = Symbol("plural!"); +const NOUN = Symbol("noun!"); const names = [ - ["World Famous", NOUN], - ["Comically Large", NOUN], - ["Comically Small", NOUN], - ["Clearance Aisle", PLURAL_NOUN], - [NOUN, "For Hire"], - ["Suspicious", NOUN], - ["Sopping Wet", PLURAL_NOUN], - ["Smelly", NOUN], - ["Friendly", NOUN], - ["Tardy", NOUN], - ["Evil", NOUN], - [PLURAL_NOUN, "That Bite"], - ["Malicious", NOUN], - ["Spiteful", NOUN], - ["Mister", NOUN], - ["Alternate", NOUN,"Universe"], - [NOUN, "Island"], - [NOUN, "Kingdom"], - [NOUN, "Empire"], - [NOUN, "Dynasty"], - [NOUN, "Cartel"], - [NOUN, "Cabal"], - ["Not Too Fond Of", PLURAL_NOUN], - ["Honk For", PLURAL_NOUN], - ["Canonically Evil", NOUN], - ["Limited Edition", NOUN], - [NOUN, "Scientist"], - ["Famous", NOUN, "Collection"], - ["Supersonic", NOUN, "Spaceship"], - ["Patent Pending", NOUN], - ["Patented", NOUN], - ["Space", NOUN], - ["Secret", NOUN, "Agenda"], - [PLURAL_NOUN, "in my walls"], - ["The", PLURAL_NOUN, "are SPIES"], - ["Traveling", NOUN, "Circus"] -] + ["World Famous", NOUN], + ["Comically Large", NOUN], + ["Comically Small", NOUN], + ["Clearance Aisle", PLURAL_NOUN], + [NOUN, "For Hire"], + ["Suspicious", NOUN], + ["Sopping Wet", PLURAL_NOUN], + ["Smelly", NOUN], + ["Friendly", NOUN], + ["Tardy", NOUN], + ["Evil", NOUN], + [PLURAL_NOUN, "That Bite"], + ["Malicious", NOUN], + ["Spiteful", NOUN], + ["Mister", NOUN], + ["Alternate", NOUN, "Universe"], + [NOUN, "Island"], + [NOUN, "Kingdom"], + [NOUN, "Empire"], + [NOUN, "Dynasty"], + [NOUN, "Cartel"], + [NOUN, "Cabal"], + ["Not Too Fond Of", PLURAL_NOUN], + ["Honk For", PLURAL_NOUN], + ["Canonically Evil", NOUN], + ["Limited Edition", NOUN], + [NOUN, "Scientist"], + ["Famous", NOUN, "Collection"], + ["Supersonic", NOUN, "Spaceship"], + ["Patent Pending", NOUN], + ["Patented", NOUN], + ["Space", NOUN], + ["Secret", NOUN, "Agenda"], + [PLURAL_NOUN, "in my walls"], + ["The", PLURAL_NOUN, "are SPIES"], + ["Traveling", NOUN, "Circus"], +]; const nouns = [ - "Snail", - "Cow", - "Giraffe", - "Donkey", - "Horse", - "Mushroom", - "Salad", - "Kitten", - "Fork", - "Apple", - "Pancake", - "Tree", - "Fern", - "Seashell", - "Turtle", - "Casserole", - "Gnome", - "Frog", -] + "Snail", + "Cow", + "Giraffe", + "Donkey", + "Horse", + "Mushroom", + "Salad", + "Kitten", + "Fork", + "Apple", + "Pancake", + "Tree", + "Fern", + "Seashell", + "Turtle", + "Casserole", + "Gnome", + "Frog", +]; function isSeedAcceptable(sanitizedSeed: number) { - let template = names[sanitizedSeed % names.length]; - let noun = nouns[Math.floor(sanitizedSeed / names.length) % nouns.length]; + const template = names[sanitizedSeed % names.length]; + const noun = nouns[Math.floor(sanitizedSeed / names.length) % nouns.length]; - let totalLength = - template.map((v) => ((v as any)?.length ?? 0)).reduce((a,b) => a + b) - + template.length - + noun.length; + const totalLength = + template.map((v) => (v as any)?.length ?? 0).reduce((a, b) => a + b) + + template.length + + noun.length; - return totalLength <= 26 + return totalLength <= 26; } /** * Generate a random username based on a numeric seed * @param seed - the seed to use to select a username * @returns a string suitable for a player username */ -export function getRandomUsername(seed: number) : string { - let sanitizedSeed = Math.floor((seed * 2999) % (names.length * nouns.length)); - let template = names[sanitizedSeed % names.length]; - let noun = nouns[Math.floor(sanitizedSeed / names.length) % nouns.length]; - let result: [string?] = []; +export function getRandomUsername(seed: number): string { + let sanitizedSeed = Math.floor((seed * 2999) % (names.length * nouns.length)); - while (!isSeedAcceptable(sanitizedSeed)) { - sanitizedSeed += 1; - } + while (!isSeedAcceptable(sanitizedSeed)) { + sanitizedSeed += 1; + } - // Convert template to some somewhat-legible word string - for (let step of template) { - if (step == PLURAL_NOUN) { - result.push(`${noun}s`); - } else if (step == NOUN) { - result.push(noun); - } else { - result.push(step.toString()); - } + const template = names[sanitizedSeed % names.length]; + const noun = nouns[Math.floor(sanitizedSeed / names.length) % nouns.length]; + const result: [string?] = []; + + // Convert template to some somewhat-legible word string + for (const step of template) { + if (step === PLURAL_NOUN) { + result.push(`${noun}s`); + } else if (step === NOUN) { + result.push(noun); + } else { + result.push(step.toString()); } + } - return result.join(" ") -} \ No newline at end of file + return result.join(" "); +} diff --git a/src/core/validations/username.ts b/src/core/validations/username.ts index 75af113c9a..12ff140fd7 100644 --- a/src/core/validations/username.ts +++ b/src/core/validations/username.ts @@ -13,12 +13,12 @@ import { translateText } from "../../client/Utils"; import { simpleHash } from "../Util"; import { getRandomUsername } from "../utilities/UsernameGenerator"; -const customDataset = new DataSet() - .addAll(englishDataset) - .addPhrase((phrase) => - phrase.setMetadata({ originalWord: 'nigg' }) +const customDataset = new DataSet().addAll(englishDataset).addPhrase((phrase) => + phrase + .setMetadata({ originalWord: "nigg" }) /* Not used by any english words */ - .addPattern(pattern`niqq`)) + .addPattern(pattern`niqq`), +); const matcher = new RegExpMatcher({ ...customDataset.build(), @@ -29,17 +29,17 @@ const matcher = new RegExpMatcher({ skipNonAlphabeticTransformer(), toAsciiLowerCaseTransformer(), collapseDuplicatesTransformer({ - customThresholds: new Map([ - ['b', 2], - ['e', 2], - ['o', 2], - ['l', 2], - ['s', 2], - ['g', 2], - ['q', 2] - ]), - }) - ] + customThresholds: new Map([ + ["b", 2], + ["e", 2], + ["o", 2], + ["l", 2], + ["s", 2], + ["g", 2], + ["q", 2], + ]), + }), + ], }); export const MIN_USERNAME_LENGTH = 3; @@ -47,16 +47,6 @@ export const MAX_USERNAME_LENGTH = 27; const validPattern = /^[a-zA-Z0-9_[\] πŸˆπŸ€ΓΌΓœ]+$/u; -const shadowNames = [ - "NicePeopleOnly", - "BeKindPlz", - "LearningManners", - "StayClassy", - "BeNicer", - "NeedHugs", - "MakeFriends", -]; - export function fixProfaneUsername(username: string): string { if (isProfaneUsername(username)) { return getRandomUsername(simpleHash(username)); diff --git a/tests/Censor.test.ts b/tests/Censor.test.ts index 74f9cccc27..ea3b71b1ed 100644 --- a/tests/Censor.test.ts +++ b/tests/Censor.test.ts @@ -1,6 +1,18 @@ // Mocking the obscenity library to control its behavior in tests. jest.mock("obscenity", () => { return { + DataSet: class { + constructor() {} + addAll() { + return this; + } + addPhrase() { + return this; + } + build() { + return {}; + } + }, RegExpMatcher: class { private dummy: string[] = ["foo", "bar", "leet", "code"]; constructor(_opts: any) {} @@ -22,6 +34,7 @@ jest.mock("obscenity", () => { resolveConfusablesTransformer: () => ({}), resolveLeetSpeakTransformer: () => ({}), skipNonAlphabeticTransformer: () => ({}), + toAsciiLowerCaseTransformer: () => ({}), }; }); @@ -41,16 +54,6 @@ import { } from "../src/core/validations/username"; describe("username.ts functions", () => { - const shadowNames = [ - "NicePeopleOnly", - "BeKindPlz", - "LearningManners", - "StayClassy", - "BeNicer", - "NeedHugs", - "MakeFriends", - ]; - describe("isProfaneUsername & fixProfaneUsername with leet decoding (mocked)", () => { test.each([ { username: "l33t", profane: true }, // decodes to "leet" @@ -76,7 +79,7 @@ describe("username.ts functions", () => { expect(fixed).toBe(username); } else { // When profane: result should be one of shadowNames - expect(shadowNames).toContain(fixed); + expect(fixed).not.toBe(username); } }); }); From 6bc55ebbda3ecfcda80762aa3613fc8c857cafe3 Mon Sep 17 00:00:00 2001 From: Joshua Date: Tue, 21 Oct 2025 09:17:58 -0700 Subject: [PATCH 3/4] More names! This bumps the total combinations of the new name generator up to 9996. --- src/core/utilities/UsernameGenerator.ts | 199 ++++++++++++++++++++++-- 1 file changed, 184 insertions(+), 15 deletions(-) diff --git a/src/core/utilities/UsernameGenerator.ts b/src/core/utilities/UsernameGenerator.ts index 73505c53e6..1b48e74afb 100644 --- a/src/core/utilities/UsernameGenerator.ts +++ b/src/core/utilities/UsernameGenerator.ts @@ -3,41 +3,154 @@ const NOUN = Symbol("noun!"); const names = [ ["World Famous", NOUN], + ["Famous", PLURAL_NOUN], ["Comically Large", NOUN], ["Comically Small", NOUN], ["Clearance Aisle", PLURAL_NOUN], - [NOUN, "For Hire"], - ["Suspicious", NOUN], - ["Sopping Wet", PLURAL_NOUN], + ["Massive", PLURAL_NOUN], ["Smelly", NOUN], ["Friendly", NOUN], ["Tardy", NOUN], ["Evil", NOUN], - [PLURAL_NOUN, "That Bite"], + ["Rude", NOUN], ["Malicious", NOUN], ["Spiteful", NOUN], ["Mister", NOUN], - ["Alternate", NOUN, "Universe"], + ["Suspicious", NOUN], + ["Sopping Wet", PLURAL_NOUN], + ["Not Too Fond Of", PLURAL_NOUN], + ["Honk For", PLURAL_NOUN], + ["Canonically Evil", NOUN], + ["Limited Edition", NOUN], + ["Patent Pending", NOUN], + ["Patented", NOUN], + ["Space", NOUN], + ["Defend The", PLURAL_NOUN], + ["Crime", PLURAL_NOUN], + ["Anarchist", NOUN], + ["Garbage", NOUN], + ["Farting", PLURAL_NOUN], + ["Suspiciously Textured", NOUN], + ["Army Of Laser", PLURAL_NOUN], + ["Republic of", PLURAL_NOUN], + ["Slippery", NOUN], + ["Wealthy", PLURAL_NOUN], + ["Politically Correct", NOUN], + ["Mall", NOUN], + ["Certified", NOUN], + ["Dr", NOUN], + ["Runaway", NOUN], + ["Chrome", NOUN], + ["All New", NOUN], + ["Top Shelf", PLURAL_NOUN], + ["Prosumer", NOUN], + ["Freshly Squeezed", NOUN], + ["Vine Ripened", NOUN], + ["Invading", PLURAL_NOUN], + ["Eau De", NOUN], + ["Freshly Showered", NOUN], + ["Loyal To", PLURAL_NOUN], + ["United States of", NOUN], + ["United States of", PLURAL_NOUN], + ["Flowing Rivers of", NOUN], + ["House of", PLURAL_NOUN], + ["Suspiciously Shaped", NOUN], + ["Fishy", NOUN], + ["Certified Organic", NOUN], + ["Unregulated", NOUN], + + [NOUN, "For Hire"], + [PLURAL_NOUN, "That Bite"], + [PLURAL_NOUN, "in my walls"], + [PLURAL_NOUN, "Are Opps"], + [NOUN, "Hotel"], + [PLURAL_NOUN, "The Movie"], + [NOUN, "Scholar"], + [NOUN, "Merchandise"], + [NOUN, "Connoisseur"], + [NOUN, "Kardashian"], + [NOUN, "Consequences"], + [NOUN, "Corporation"], + [PLURAL_NOUN, "Inc"], + [NOUN, "Democracy"], + [NOUN, "Network"], + [NOUN, "Railway"], + [NOUN, "Congress"], + [NOUN, "Alliance"], [NOUN, "Island"], [NOUN, "Kingdom"], [NOUN, "Empire"], [NOUN, "Dynasty"], [NOUN, "Cartel"], [NOUN, "Cabal"], - ["Not Too Fond Of", PLURAL_NOUN], - ["Honk For", PLURAL_NOUN], - ["Canonically Evil", NOUN], - ["Limited Edition", NOUN], + [NOUN, "Land"], + [NOUN, "Oligarchy"], [NOUN, "Scientist"], + [NOUN, "Seeking Missile"], + [NOUN, "Post Office"], + [NOUN, "Nationalist"], + [NOUN, "State"], + [NOUN, "Duchy"], + [NOUN, "Ocean"], + + ["Alternate", NOUN, "Universe"], + ["Let That", NOUN, "In"], ["Famous", NOUN, "Collection"], ["Supersonic", NOUN, "Spaceship"], - ["Patent Pending", NOUN], - ["Patented", NOUN], - ["Space", NOUN], ["Secret", NOUN, "Agenda"], - [PLURAL_NOUN, "in my walls"], + ["Ballistic", NOUN, "Missile"], ["The", PLURAL_NOUN, "are SPIES"], ["Traveling", NOUN, "Circus"], + ["The", PLURAL_NOUN, "Lied"], + ["Casual", NOUN, "Enthusiast"], + ["Sacred", NOUN, "Knowledge"], + ["Quantum", NOUN, "Computer"], + ["Hadron", NOUN, "Collider"], + ["Large", NOUN, "Obliterator"], + ["Interstellar", NOUN, "Cabal"], + ["Interstellar", NOUN, "Army"], + ["Interstellar", NOUN, "Pirates"], + ["Interstellar", NOUN, "Dynasty"], + ["Interstellar", NOUN, "Clan"], + ["Galactic", NOUN, "Smugglers"], + ["Galactic", NOUN, "Cabal"], + ["Galactic", NOUN, "Alliance"], + ["Galactic", NOUN, "Empire"], + ["Galactic", NOUN, "Army"], + ["Galactic", NOUN, "Crown"], + ["Galactic", NOUN, "Pirates"], + ["Galactic", NOUN, "Dynasty"], + ["Galactic", NOUN, "Clan"], + ["Alien", NOUN, "Army"], + ["Alien", NOUN, "Cabal"], + ["Alien", NOUN, "Alliance"], + ["Alien", NOUN, "Empire"], + ["Alien", NOUN, "Pirates"], + ["Alien", NOUN, "Clan"], + ["Grand", NOUN, "Empire"], + ["Grand", NOUN, "Dynasty"], + ["Grand", NOUN, "Army"], + ["Grand", NOUN, "Cabal"], + ["Grand", NOUN, "Alliance"], + ["Royal", NOUN, "Army"], + ["Royal", NOUN, "Cabal"], + ["Royal", NOUN, "Empire"], + ["Royal", NOUN, "Dynasty"], + ["Holy", NOUN, "Dynasty"], + ["Holy", NOUN, "Empire"], + ["Holy", NOUN, "Alliance"], + ["Eternal", NOUN, "Empire"], + ["Eternal", NOUN, "Cabal"], + ["Eternal", NOUN, "Alliance"], + ["Eternal", NOUN, "Dynasty"], + ["Invading", NOUN, "Cabal"], + ["Invading", NOUN, "Empire"], + ["Invading", NOUN, "Alliance"], + ["Immortal", NOUN, "Pirates"], + ["Shadow", NOUN, "Cabal"], + ["Secret", NOUN, "Dynasty"], + ["The Great", NOUN, "Army"], + ["The", NOUN, "Matrix"], ]; const nouns = [ @@ -59,6 +172,56 @@ const nouns = [ "Casserole", "Gnome", "Frog", + "Cheese", + "Mold", + "Clown", + "Boat", + "Moron", + "Robot", + "Millionaire", + "Billionaire", + "Pigeon", + "Fish", + "Bumblebee", + "Jelly", + "Wizard", + "Worm", + "Rat", + "Pumpkin", + "Zombie", + "Grass", + "Bear", + "Skunk", + "Sandwich", + "Butter", + "Soda", + "Pickle", + "Potato", + "Book", + "Friend", + "Feather", + "Flower", + "Oil", + "Train", + "Fan", + "Hater", + "Opp", + "Salmon", + "Cod", + "Sink", + "Villain", + "Bug", + "Car", + "Soup", + "Puppy", + "Rock", + "Stick", + "Succulent", + "Nerd", + "Mercenary", + "Ninja", + "Burger", + "Tomato", ]; function isSeedAcceptable(sanitizedSeed: number) { @@ -78,7 +241,10 @@ function isSeedAcceptable(sanitizedSeed: number) { * @returns a string suitable for a player username */ export function getRandomUsername(seed: number): string { - let sanitizedSeed = Math.floor((seed * 2999) % (names.length * nouns.length)); + // note: ONLY use prime numbers here + let sanitizedSeed = Math.floor( + (seed * 19991) % (names.length * nouns.length), + ); while (!isSeedAcceptable(sanitizedSeed)) { sanitizedSeed += 1; @@ -91,7 +257,10 @@ export function getRandomUsername(seed: number): string { // Convert template to some somewhat-legible word string for (const step of template) { if (step === PLURAL_NOUN) { - result.push(`${noun}s`); + if (noun.endsWith("s")) result.push(`${noun}es`); + else { + result.push(`${noun}s`); + } } else if (step === NOUN) { result.push(noun); } else { From 88b55065fa8591431ad4cfa77be2a882b3c4657f Mon Sep 17 00:00:00 2001 From: Joshua Date: Tue, 21 Oct 2025 09:34:49 -0700 Subject: [PATCH 4/4] Add some more edgy terms to the filter These new terms will filter out stuff like "shoot __" or "kill the __". Optional commit, not required for PR. --- src/core/validations/username.ts | 53 ++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/core/validations/username.ts b/src/core/validations/username.ts index 12ff140fd7..5a96142e77 100644 --- a/src/core/validations/username.ts +++ b/src/core/validations/username.ts @@ -13,12 +13,53 @@ import { translateText } from "../../client/Utils"; import { simpleHash } from "../Util"; import { getRandomUsername } from "../utilities/UsernameGenerator"; -const customDataset = new DataSet().addAll(englishDataset).addPhrase((phrase) => - phrase - .setMetadata({ originalWord: "nigg" }) - /* Not used by any english words */ - .addPattern(pattern`niqq`), -); +const customDataset = new DataSet() + .addAll(englishDataset) + /* similarity to racial slur */ + .addPhrase((phrase) => + phrase + .setMetadata({ originalWord: "nigg" }) + /* Not used by any english words */ + .addPattern(pattern`niqq`), + ) + /* historic significance / edgy */ + .addPhrase((phrase) => + phrase + .setMetadata({ originalWord: "hitler" }) + .addPattern(pattern`hitl?r`) + .addPattern(pattern`hiti?r`) + .addPattern(pattern`hltl?r`), + ) + .addPhrase((phrase) => + phrase.setMetadata({ originalWord: "nazi" }).addPattern(pattern`|nazi`), + ) + /* aggressive / edgy */ + .addPhrase((phrase) => + phrase.setMetadata({ originalWord: "hang" }).addPattern(pattern`|hang|`), + ) + .addPhrase((phrase) => + phrase + .setMetadata({ originalWord: "kill" }) + .addPattern(pattern`|kill`) + /* not used by any english words */ + .addPattern(pattern`ikill`), + ) + .addPhrase((phrase) => + phrase + .setMetadata({ originalWord: "murder" }) + /* only used by a few english words */ + .addPattern(pattern`murd`) + .addPattern(pattern`mard`), + ) + .addPhrase((phrase) => + phrase + .setMetadata({ originalWord: "shoot" }) + .addPattern(pattern`|shoot`) + .addPattern(pattern`|shot`) + /* only used by a few english words */ + .addPattern(pattern`ishoot`) + .addPattern(pattern`ishot`), + ); const matcher = new RegExpMatcher({ ...customDataset.build(),