Make zoacao match rule editable in-game: mode + pattern + message list

Zoacao now matches a configurable pattern under a configurable mode
(igual/contem/comeca/termina/regex) instead of only a bare 'f'. The mode,
the trigger pattern, and the gag message list are all editable live via
/canalhandia zoacao <listar|modo|padrao|adicionar|remover|limpar>; every
change writes through to config.yml immediately. config.yml gains
zoacao.correspondencia + zoacao.padrao (default igual + 'f').

Zoacao.replace/ matches gained a Mode + pattern signature; ZoacaoTest
covers all five modes (incl. invalid-regex guard) and the byKey parser.
136 tests green.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
marcos
2026-08-06 22:27:42 +00:00
parent 72840c9770
commit b77a38b394
7 changed files with 351 additions and 45 deletions
+7 -1
View File
@@ -25,7 +25,7 @@ All player-facing text is Portuguese (pt-BR).
| `ranking` | `/ranking mineracao` and friends. Covers **offline players too**. |
| `marcos` | Announces round milestones — 100 km walked, 24 hours played — the first time someone crosses one. |
| `mortes` | Replaces each death message with a comic pt-BR line (cause-based flavor + a death counter) and sends the death coordinates **privately** to the dead player on respawn (the death screen swallows chat sent during the event), so they can run back to their dropped items. Respects `keepInventory`. No storage, no command. |
| `zoacao` | A bare `f`/`F` in chat (trimmed, nothing else) is swapped for a random line from `zoacao.mensagens` — a chat gag. The player's name still prefixes it. Pure chat swap; the `luto` tribute is unaffected (paying respects still needs the `[F]` button or `/f`). Affects Bedrock chat too (it's a chat event, not a click). |
| `zoacao` | A chat message matching the trigger (a pattern + a match mode) is swapped for a random line from `zoacao.mensagens` — a chat gag. The player's name still prefixes it. Default: a bare `f`/`F` (trimmed) → a random gag line. Match modes: `igual` (equals), `contem` (contains), `comeca` (starts with), `termina` (ends with), `regex`. The mode, the pattern, and the message list are all editable in-game with `/canalhandia zoacao ...`. Pure chat swap; the `luto` tribute is unaffected (paying respects still needs the `[F]` button or `/f`). Affects Bedrock chat too (it's a chat event, not a click). |
| `ia` | `/ia <pergunta>` asks an OpenAI-compatible model in chat. Optional wiki grounding, per-player memory, operator corrections. |
Toggle any of them: `/canalhandia modulo <nome> <on|off>`
@@ -164,6 +164,12 @@ Admin (`canalhandia.admin`):
/canalhandia modulo <nome> <on|off> liga/desliga um módulo
/canalhandia marcos força uma verificação de marcos
/canalhandia limpar [cooldown|historico|tudo]
/canalhandia zoacao listar mostra a regra e as frases da zoação
/canalhandia zoacao modo <m> igual | contem | comeca | termina | regex
/canalhandia zoacao padrao <texto> texto/regex que dispara a zoação
/canalhandia zoacao adicionar <frase> adiciona uma frase de zoação
/canalhandia zoacao remover <n|texto> remove uma frase de zoação
/canalhandia zoacao limpar volta para as frases padrão
/canalhandia reload
/curiosidade modo <entrada|intervalo|ambos|manual>
/curiosidade intervalo <min> intervalo do modo temporizado
@@ -480,17 +480,20 @@ public final class Canalhandia extends JavaPlugin implements Listener {
}
/**
* Chat gag: a bare "f" (or "F", trimmed, nothing else) gets replaced with a
* random line from {@code zoacao.mensagens}. Pure chat swap — the player's
* name still prefixes it as normal. Independent of {@code luto}: paying
* respects still needs the {@code [F]} button (Java) or {@code /f} command.
* Chat gag: a message matching the {@code zoacao} trigger (pattern + match
* mode, default a bare "f") gets replaced with a random line from
* {@code zoacao.mensagens}. Pure chat swap — the player's name still
* prefixes it as normal. Independent of {@code luto}: paying respects still
* needs the {@code [F]} button (Java) or {@code /f} command. Editable
* in-game via {@code /canalhandia zoacao ...}.
*/
@EventHandler
public void onChatF(AsyncPlayerChatEvent event) {
if (!settings.moduleEnabled(Module.ZOACAO)) {
return;
}
String gag = Zoacao.replace(event.getMessage(), settings.zoacaoMessages(), random);
String gag = Zoacao.replace(event.getMessage(), settings.zoacaoMode(),
settings.zoacaoPattern(), settings.zoacaoMessages(), random);
if (gag != null) {
event.setMessage(gag);
}
@@ -89,6 +89,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
}
case "limpar" -> clear(sender, rest);
case "zoacao" -> zoacaoEdit(sender, rest);
case "reload" -> {
if (admin(sender)) {
plugin.reloadConfig();
@@ -619,6 +620,97 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
}
/**
* In-game editing of the {@code zoacao} chat gag: the match mode, the
* trigger pattern, and the replacement message list. Admin-only; every
* change writes through to config.yml immediately.
*/
private void zoacaoEdit(CommandSender sender, String[] args) {
if (!admin(sender)) {
return;
}
Settings settings = plugin.settings();
String sub = args.length > 0 ? args[0].toLowerCase(Locale.ROOT) : "listar";
switch (sub) {
case "listar", "status" -> {
Msg.header(sender, "Zoação (gag de chat)");
Msg.line(sender, "correspondência", settings.zoacaoMode().key()
+ " (igual | contem | comeca | termina | regex)");
Msg.line(sender, "padrão", settings.zoacaoPattern());
List<String> messages = settings.zoacaoMessages();
Msg.line(sender, "mensagens", "(" + messages.size() + ")");
for (int i = 0; i < messages.size(); i++) {
sender.sendMessage(Component.text(" " + (i + 1) + ". ", NamedTextColor.DARK_AQUA)
.append(Component.text(messages.get(i), NamedTextColor.WHITE)));
}
}
case "modo" -> {
if (args.length < 2) {
Msg.error(sender, "Uso: /canalhandia zoacao modo <igual|contem|comeca|termina|regex>");
return;
}
Zoacao.Mode mode = Zoacao.Mode.byKey(args[1]);
if (mode == null) {
Msg.error(sender, "Modo inválido. Use: igual, contem, comeca, termina, regex.");
return;
}
settings.zoacaoMode(mode);
Msg.ok(sender, "Correspondência da zoação: " + mode.key() + ".");
}
case "padrao" -> {
if (args.length < 2) {
Msg.error(sender, "Uso: /canalhandia zoacao padrao <texto> (atual: " + settings.zoacaoPattern() + ")");
return;
}
String pattern = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
settings.zoacaoPattern(pattern);
Msg.ok(sender, "Padrão da zoação: " + pattern + ".");
}
case "adicionar", "add" -> {
if (args.length < 2) {
Msg.error(sender, "Uso: /canalhandia zoacao adicionar <frase>");
return;
}
String line = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
List<String> messages = new ArrayList<>(settings.zoacaoMessages());
messages.add(line);
settings.zoacaoMessages(messages);
Msg.ok(sender, "Adicionado: " + line + " (agora " + messages.size() + " mensagens).");
}
case "remover", "remove" -> {
List<String> messages = new ArrayList<>(settings.zoacaoMessages());
if (args.length < 2) {
Msg.error(sender, "Uso: /canalhandia zoacao remover <número|texto>");
return;
}
String target = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
int index = parse(target, Integer.MIN_VALUE);
boolean removed;
if (index != Integer.MIN_VALUE) {
int idx = index - 1;
removed = idx >= 0 && idx < messages.size();
if (removed) {
messages.remove(idx);
}
} else {
removed = messages.removeIf(m -> m.equalsIgnoreCase(target));
}
if (!removed) {
Msg.error(sender, "Não encontrei '" + target + "' na lista.");
return;
}
settings.zoacaoMessages(messages);
Msg.ok(sender, "Removido. Restam " + messages.size() + " mensagens.");
}
case "limpar" -> {
// Clear the configured list so the built-in defaults come back.
settings.zoacaoMessages(List.of());
Msg.ok(sender, "Lista limpa — voltou para as mensagens padrão.");
}
default -> Msg.error(sender, "Uso: /canalhandia zoacao <listar|modo|padrao|adicionar|remover|limpar>");
}
}
private void clear(CommandSender sender, String[] args) {
if (!admin(sender)) {
return;
@@ -666,6 +758,8 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
}
Msg.line(sender, "categorias ativas", enabled.isEmpty() ? "nenhuma" : String.join(", ", enabled));
Msg.line(sender, "zoação", settings.zoacaoMode().key() + " '" + settings.zoacaoPattern()
+ "' (" + settings.zoacaoMessages().size() + " mensagens)");
Msg.line(sender, "ia", plugin.ai().configured()
? settings.aiModel() + " · perfil " + settings.aiProfile().name().toLowerCase(Locale.ROOT)
+ " · " + plugin.ai().askedToday() + "/" + settings.aiDailyLimit() + " hoje"
@@ -724,6 +818,12 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
commands.put("/curiosidade reacao add <chave> <rótulo>", "cria ou muda uma reação");
commands.put("/curiosidade reacao remover <chave>", "remove uma reação");
commands.put("/curiosidade categoria <nome> <on|off>", "liga/desliga uma categoria");
commands.put("/canalhandia zoacao listar", "mostra a regra e as frases da zoação");
commands.put("/canalhandia zoacao modo <m>", "igual | contem | comeca | termina | regex");
commands.put("/canalhandia zoacao padrao <texto>", "texto/regex que dispara a zoação");
commands.put("/canalhandia zoacao adicionar <frase>", "adiciona uma frase de zoação");
commands.put("/canalhandia zoacao remover <n|texto>", "remove uma frase de zoação");
commands.put("/canalhandia zoacao limpar", "volta para as frases padrão");
}
commands.forEach((cmd, description) -> sender.sendMessage(
Component.text(" " + cmd, NamedTextColor.AQUA)
@@ -893,7 +993,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
if (args.length == 1) {
List<String> options = new ArrayList<>(List.of("status", "modulos", "plataformas"));
if (sender.hasPermission(ADMIN)) {
options.addAll(List.of("modulo", "marcos", "limpar", "reload"));
options.addAll(List.of("modulo", "marcos", "limpar", "zoacao", "reload"));
}
return filter(options, args[0]);
}
@@ -907,6 +1007,12 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
if (args.length == 2 && args[0].equalsIgnoreCase("limpar")) {
return filter(List.of("cooldown", "historico", "tudo"), args[1]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("zoacao")) {
return filter(List.of("listar", "modo", "padrao", "adicionar", "remover", "limpar"), args[1]);
}
if (args.length == 3 && args[0].equalsIgnoreCase("zoacao") && args[1].equalsIgnoreCase("modo")) {
return filter(List.of("igual", "contem", "comeca", "termina", "regex"), args[2]);
}
if (args.length == 3 && args[0].equalsIgnoreCase("modulo")) {
return filter(List.of("on", "off"), args[2]);
}
@@ -347,9 +347,9 @@ final class Settings {
// --- zoacao (f-gag) -----------------------------------------------------
/**
* Lines a bare "f" in chat gets replaced with, picked at random. Defaults
* to a small built-in list if unset/empty so the feature works out of the
* box; operators edit {@code zoacao.mensagens} in config.yml to customise.
* Lines a matching chat message gets replaced with, picked at random.
* Defaults to a small built-in list if unset/empty so the feature works out
* of the box; operators edit {@code zoacao.mensagens} in-game to customise.
*/
List<String> zoacaoMessages() {
List<String> messages = plugin.getConfig().getStringList("zoacao.mensagens");
@@ -365,6 +365,31 @@ final class Settings {
return messages;
}
/** Writes the full message list through to config immediately. */
void zoacaoMessages(List<String> messages) {
plugin.getConfig().set("zoacao.mensagens", messages);
plugin.saveConfig();
}
/** How a chat message is tested against the trigger pattern. */
Zoacao.Mode zoacaoMode() {
return Zoacao.Mode.byKeyOrDefault(plugin.getConfig().getString("zoacao.correspondencia", "igual"),
Zoacao.Mode.IGUAL);
}
void zoacaoMode(Zoacao.Mode mode) {
set("zoacao.correspondencia", mode.key());
}
/** The trigger text (or regex for the {@code regex} mode). */
String zoacaoPattern() {
return plugin.getConfig().getString("zoacao.padrao", "f");
}
void zoacaoPattern(String pattern) {
set("zoacao.padrao", pattern);
}
// --- content ------------------------------------------------------------
boolean categoryEnabled(Category category) {
@@ -1,37 +1,110 @@
package dev.marcospaulo.canalhandia;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.regex.Pattern;
/**
* Pure logic for the {@code zoacao} chat gag: when a player sends a bare
* {@code "f"} (or {@code "F"}, trimmed, nothing else) their message is swapped
* for a random line from a configurable list. Extracted so the trigger rule is
* Pure logic for the {@code zoacao} chat gag: when a player's chat message
* matches a configurable trigger (a pattern + a match mode), it is swapped
* for a random line from a configurable list. Extracted so the match rule is
* unit-testable without a server.
*
* <p>Match modes:
* <ul>
* <li>{@link Mode#IGUAL} — message equals the pattern (ignoring case, trimmed)</li>
* <li>{@link Mode#CONTEM} — message contains the pattern (case-insensitive)</li>
* <li>{@link Mode#COMECA} — message starts with the pattern</li>
* <li>{@link Mode#TERMINA} — message ends with the pattern</li>
* <li>{@link Mode#REGEX} — pattern is a case-insensitive regex, matched anywhere</li>
* </ul>
*
* <p>This is a standalone chat gag. It does not interact with the {@code luto}
* tribute — paying respects still happens via the {@code [F]} button (Java) or
* the {@code /f} command (Bedrock), neither of which is a chat message.
*/
final class Zoacao {
/** How a chat message is tested against the trigger pattern. */
enum Mode {
IGUAL("igual"),
CONTEM("contem"),
COMECA("comeca"),
TERMINA("termina"),
REGEX("regex");
private final String key;
Mode(String key) {
this.key = key;
}
String key() {
return key;
}
static Mode byKey(String key) {
for (Mode mode : values()) {
if (mode.key.equalsIgnoreCase(key)) {
return mode;
}
}
return null;
}
static Mode byKeyOrDefault(String key, Mode fallback) {
Mode mode = byKey(key);
return mode == null ? fallback : mode;
}
}
private Zoacao() {
}
/**
* @param message the chat message as sent by the player
* @param mode how to test the message against the pattern
* @param pattern the trigger text (or regex for {@link Mode#REGEX})
* @param gags the configured replacement lines; if null/empty, no gag
* @param random shared random used to pick a line
* @return the replacement line if the message is a bare "f", otherwise null
* @return the replacement line if the message matches, otherwise null
* (meaning "leave the message alone")
*/
static String replace(String message, List<String> gags, Random random) {
if (message == null || gags == null || gags.isEmpty()) {
static String replace(String message, Mode mode, String pattern, List<String> gags, Random random) {
if (message == null || gags == null || gags.isEmpty() || pattern == null || pattern.isBlank()) {
return null;
}
if (!message.trim().equalsIgnoreCase("f")) {
if (!matches(message, mode, pattern)) {
return null;
}
return gags.get(random.nextInt(gags.size()));
}
/** Pure test of one message against one pattern under one mode. */
static boolean matches(String message, Mode mode, String pattern) {
if (message == null || mode == null || pattern == null) {
return false;
}
String trimmed = message.trim();
String needle = pattern.toLowerCase(Locale.ROOT);
switch (mode) {
case IGUAL:
return trimmed.equalsIgnoreCase(pattern);
case CONTEM:
return trimmed.toLowerCase(Locale.ROOT).contains(needle);
case COMECA:
return trimmed.toLowerCase(Locale.ROOT).startsWith(needle);
case TERMINA:
return trimmed.toLowerCase(Locale.ROOT).endsWith(needle);
case REGEX:
try {
return Pattern.compile(pattern, Pattern.CASE_INSENSITIVE).matcher(trimmed).find();
} catch (java.util.regex.PatternSyntaxException e) {
return false;
}
default:
return false;
}
}
}
+13 -2
View File
@@ -71,9 +71,20 @@ janela-reacao-segundos: 90
luto:
cabeca: true
# Frases que substituem um "f" sozinho no chat (módulo zoacao). Uma é sorteada
# por mensagem. Edite à vontade — a graça é ser inesperado.
# Zoa de quem manda uma mensagem que bate com o padrão, trocando por uma frase
# engraçada. Tudo editável em jogo com /canalhandia zoacao ...
zoacao:
# Como comparar a mensagem do chat com o padrão:
# igual - mensagem inteira igual ao padrão (ignora maiúsculas e espaços)
# contem - mensagem contém o padrão em qualquer lugar
# comeca - mensagem começa com o padrão
# termina - mensagem termina com o padrão
# regex - padrão é uma expressão regular (maiúsculas ignoradas)
correspondencia: igual
# Texto (ou regex) que dispara a zoação. Padrão: só um "f" sozinho.
padrao: "f"
# Frases que substituem a mensagem. Uma é sorteada por vez. Edite à vontade —
# a graça é ser inesperado.
mensagens:
- "Sou gay"
- "Gosto de anime"
@@ -1,6 +1,7 @@
package dev.marcospaulo.canalhandia;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -13,48 +14,129 @@ class ZoacaoTest {
private static final List<String> GAGS = List.of(
"Sou gay", "Gosto de anime", "Jogo no celular");
// --- Mode.byKey ---------------------------------------------------------
@Test
void bareLowercaseFIsReplacedWithAGagFromTheList() {
String gag = Zoacao.replace("f", GAGS, new Random(0L));
void modeByKeyParsesEachMode() {
assertEquals(Zoacao.Mode.IGUAL, Zoacao.Mode.byKey("igual"));
assertEquals(Zoacao.Mode.CONTEM, Zoacao.Mode.byKey("contem"));
assertEquals(Zoacao.Mode.COMECA, Zoacao.Mode.byKey("comeca"));
assertEquals(Zoacao.Mode.TERMINA, Zoacao.Mode.byKey("termina"));
assertEquals(Zoacao.Mode.REGEX, Zoacao.Mode.byKey("regex"));
}
@Test
void modeByKeyIsCaseInsensitive() {
assertEquals(Zoacao.Mode.CONTEM, Zoacao.Mode.byKey("CONTEM"));
}
@Test
void modeByKeyReturnsNullForUnknown() {
assertNull(Zoacao.Mode.byKey("exato"));
}
@Test
void modeByKeyOrDefaultFallsBack() {
assertEquals(Zoacao.Mode.IGUAL, Zoacao.Mode.byKeyOrDefault("xx", Zoacao.Mode.IGUAL));
assertEquals(Zoacao.Mode.CONTEM, Zoacao.Mode.byKeyOrDefault("contem", Zoacao.Mode.IGUAL));
}
// --- matches / replace: IGUAL ------------------------------------------
@Test
void igualBareLowercaseFMatches() {
assertTrue(Zoacao.matches("f", Zoacao.Mode.IGUAL, "f"));
}
@Test
void igualBareUppercaseFMatches() {
assertTrue(Zoacao.matches("F", Zoacao.Mode.IGUAL, "f"));
}
@Test
void igualSurroundingWhitespaceStillMatches() {
assertTrue(Zoacao.matches(" f ", Zoacao.Mode.IGUAL, "f"));
assertTrue(Zoacao.matches("\tF\n", Zoacao.Mode.IGUAL, "f"));
}
@Test
void igualFWithAnythingElseDoesNotMatch() {
assertFalse(Zoacao.matches("f lol", Zoacao.Mode.IGUAL, "f"));
assertFalse(Zoacao.matches("ff", Zoacao.Mode.IGUAL, "f"));
assertFalse(Zoacao.matches("pra você f", Zoacao.Mode.IGUAL, "f"));
}
// --- matches: CONTEM / COMECA / TERMINA ---------------------------------
@Test
void contemMatchesAnywhere() {
assertTrue(Zoacao.matches("aaaffffaaa", Zoacao.Mode.CONTEM, "fff"));
assertTrue(Zoacao.matches("morte do f cara", Zoacao.Mode.CONTEM, "f"));
assertFalse(Zoacao.matches("oi", Zoacao.Mode.CONTEM, "f"));
}
@Test
void comecaMatchesAtStart() {
assertTrue(Zoacao.matches("f para o morto", Zoacao.Mode.COMECA, "f"));
assertTrue(Zoacao.matches("FFFreak", Zoacao.Mode.COMECA, "f"));
assertFalse(Zoacao.matches("oi f", Zoacao.Mode.COMECA, "f"));
}
@Test
void terminaMatchesAtEnd() {
assertTrue(Zoacao.matches("press f", Zoacao.Mode.TERMINA, "f"));
assertTrue(Zoacao.matches("mais F", Zoacao.Mode.TERMINA, "f"));
assertFalse(Zoacao.matches("f oi", Zoacao.Mode.TERMINA, "f"));
}
// --- matches: REGEX -----------------------------------------------------
@Test
void regexMatchesAnywhereCaseInsensitive() {
assertTrue(Zoacao.matches("drop f na fogueira", Zoacao.Mode.REGEX, "\\bf\\b"));
assertTrue(Zoacao.matches("FFFFFFFF", Zoacao.Mode.REGEX, "f+"));
assertFalse(Zoacao.matches("floresta", Zoacao.Mode.REGEX, "^f$"));
}
@Test
void regexInvalidPatternDoesNotMatch() {
assertFalse(Zoacao.matches("f", Zoacao.Mode.REGEX, "(["));
}
// --- replace -----------------------------------------------------------
@Test
void replaceReturnsAGagWhenMatch() {
String gag = Zoacao.replace("f", Zoacao.Mode.IGUAL, "f", GAGS, new Random(0L));
assertTrue(GAGS.contains(gag), "expected a gag from the list, got " + gag);
}
@Test
void bareUppercaseFIsReplaced() {
// A single-element list makes the random pick deterministic.
String gag = Zoacao.replace("F", List.of("alvo"), new Random(0L));
assertEquals("alvo", gag);
void replaceSingleElementListIsDeterministic() {
assertEquals("alvo",
Zoacao.replace("f", Zoacao.Mode.IGUAL, "f", List.of("alvo"), new Random(0L)));
}
@Test
void surroundingWhitespaceStillCountsAsBareF() {
assertEquals("alvo", Zoacao.replace(" f ", List.of("alvo"), new Random(0L)));
assertEquals("alvo", Zoacao.replace("\tF\n", List.of("alvo"), new Random(0L)));
void replaceReturnsNullWhenNoMatch() {
assertNull(Zoacao.replace("oi", Zoacao.Mode.IGUAL, "f", GAGS, new Random(0L)));
}
@Test
void fWithAnythingElseIsLeftAlone() {
assertNull(Zoacao.replace("f lol", GAGS, new Random(0L)));
assertNull(Zoacao.replace("ff", GAGS, new Random(0L)));
assertNull(Zoacao.replace("f.", GAGS, new Random(0L)));
assertNull(Zoacao.replace("pra você f", GAGS, new Random(0L)));
void replaceReturnsNullForEmptyOrNullGags() {
assertNull(Zoacao.replace("f", Zoacao.Mode.IGUAL, "f", List.of(), new Random(0L)));
assertNull(Zoacao.replace("f", Zoacao.Mode.IGUAL, "f", null, new Random(0L)));
}
@Test
void nonFMessagesAreLeftAlone() {
assertNull(Zoacao.replace("oi", GAGS, new Random(0L)));
assertNull(Zoacao.replace("", GAGS, new Random(0L)));
assertNull(Zoacao.replace("F para o morto", GAGS, new Random(0L)));
void replaceReturnsNullForBlankOrNullPattern() {
assertNull(Zoacao.replace("f", Zoacao.Mode.IGUAL, "", GAGS, new Random(0L)));
assertNull(Zoacao.replace("f", Zoacao.Mode.IGUAL, " ", GAGS, new Random(0L)));
assertNull(Zoacao.replace("f", Zoacao.Mode.IGUAL, null, GAGS, new Random(0L)));
}
@Test
void emptyOrNullGagsLeaveMessageAlone() {
assertNull(Zoacao.replace("f", List.of(), new Random(0L)));
assertNull(Zoacao.replace("f", null, new Random(0L)));
}
@Test
void nullMessageLeftAlone() {
assertNull(Zoacao.replace(null, GAGS, new Random(0L)));
void replaceReturnsNullForNullMessage() {
assertNull(Zoacao.replace(null, Zoacao.Mode.IGUAL, "f", GAGS, new Random(0L)));
}
}