import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.l2jmobius.gameserver.managers.FakePlayerBotContext; import org.l2jmobius.gameserver.managers.FakePlayerChatLines; import org.l2jmobius.gameserver.managers.FakePlayerHeardManager; import org.l2jmobius.gameserver.managers.FakePlayerIdiolect; import org.l2jmobius.gameserver.managers.FakePlayerIntentParser; /** * Offline harness for the bot chat engine: runs the real server classes * (FakePlayerChatLines, FakePlayerIntentParser, FakePlayerIdiolect, * FakePlayerBotContext, FakePlayerHeardManager) against the real XML data * without a game server. Run from a directory that contains data/ (see * tools/run_harness.sh). Exit code 1 on any failed check. */ public class ChatHarness { private static int failures = 0; private static int checks = 0; private static void check(boolean condition, String what) { checks++; if (!condition) { failures++; System.out.println(" FAIL: " + what); } } public static void main(String[] args) { final FakePlayerChatLines lines = FakePlayerChatLines.getInstance(); final FakePlayerIntentParser intents = FakePlayerIntentParser.getInstance(); // 1. Every seam the code uses must exist and produce a line with full slots. final String[] codeSeams = { "greeting", "whisper_default", "reply_where", "reply_price", "reply_help_yes", "reply_help_no", "reply_party_no", "party_accept", "reply_insult_soft", "reply_insult_aggro", "reply_thanks", "reply_botcheck", "reply_duel", "reply_rumor_none", "reply_bye", "reply_yes_ok", "reply_how", "reply_caps", "reply_again", "overheard_q", "reply_level", "gank_start", "pkk_start", "war_start", "killed_by", "revenge_meet", "friendly_meet", "ambient", "levelup", "grats", "migrate", "raid_start", "trade_shout", "invite_player", "call_help", "lfp", "overheard", "assist", "companion_done", "store_title", "bot_talk", "reply_drop", "siege_start", "siege_defend", "siege_gate", "siege_engrave", "siege_win", "siege_lose", "profession", "died_mob", "loot_brag", "reply_clan", "reply_solo", "reply_long", "reply_pk" }; final Map slots = new HashMap<>(); slots.put("player", "Vanger"); slots.put("target", "Vanger"); slots.put("level", "42"); slots.put("boss", "Core"); slots.put("zone", "Cruma"); slots.put("town", "Giran"); slots.put("msg", "продам дроп"); slots.put("prev", "не пойду"); slots.put("enemyclan", "RedCrew"); slots.put("mob", "Cruma Marshlands Traitor"); slots.put("castle", "Giran"); slots.put("clan", "IronPact"); slots.put("class", "Gladiator"); slots.put("drop", "Oriharukon Ore"); slots.put("item", "соски D"); slots.put("price", "50к"); slots.put("crowd", "6"); System.out.println("== seams used by code (" + codeSeams.length + ") =="); final Map samples = new TreeMap<>(); for (String seam : codeSeams) { String ok = null; for (int botId = 1; (botId <= 12) && (ok == null); botId++) { final Map facts = new HashMap<>(); facts.put("personality", (botId % 4 == 0) ? "ganker" : (botId % 4 == 1) ? "helper" : (botId % 4 == 2) ? "pkk" : "neutral"); facts.put("asker", (botId % 3 == 0) ? "stranger" : (botId % 3 == 1) ? "clanmate" : "killed_me"); ok = lines.speak(botId, seam, facts, slots, false, null); } check(ok != null, "seam produces nothing: " + seam); if (ok != null) { samples.put(seam, ok); } } for (Map.Entry entry : samples.entrySet()) { System.out.println(" " + entry.getKey() + ": " + entry.getValue()); } // 2. Context facts change what a bot says (plan / event / grounding slots). System.out.println("== context =="); FakePlayerBotContext.setPlan(77, "siege", 60000); FakePlayerBotContext.setSlot(77, "castle", "Aden"); int siegeMentions = 0; for (int i = 0; i < 40; i++) { final String line = lines.speak(77, "ambient", Map.of("personality", "neutral"), null, false, null); if ((line != null) && (line.contains("осад") || line.contains("Aden"))) { siegeMentions++; } } System.out.println(" plan=siege -> siege mentions in 40 ambient lines: " + siegeMentions); check(siegeMentions >= 8, "plan=siege rule should fire regularly (got " + siegeMentions + ")"); FakePlayerBotContext.setPlan(78, "farm", 0); FakePlayerBotContext.setEvent(78, "profession"); int professionMentions = 0; for (int i = 0; i < 40; i++) { final String line = lines.speak(78, "ambient", Map.of("personality", "neutral"), null, false, null); if ((line != null) && line.contains("проф")) { professionMentions++; } } System.out.println(" event=profession -> mentions in 40 ambient lines: " + professionMentions); check(professionMentions >= 2, "event=profession rule should fire (got " + professionMentions + ")"); FakePlayerBotContext.setSlot(79, "drop", "Adamantite Nugget"); FakePlayerBotContext.setSlot(79, "mob", "Dion Grizzly"); final String dropLine = lines.speak(79, "reply_drop", Map.of("personality", "neutral"), Map.of("player", "Vanger"), false, null); System.out.println(" reply_drop with grounding slots: " + dropLine); check(dropLine != null, "reply_drop with slots from context"); final String noDrop = lines.speak(80, "reply_drop", Map.of("personality", "neutral"), Map.of("player", "Vanger"), false, null); System.out.println(" reply_drop without drop slot: " + noDrop); check(noDrop != null, "reply_drop must have slot-free lines"); // 3. Bot to bot openers are understood by the listener (intent) or fall back to overheard. System.out.println("== bot_talk -> intents =="); final Map distribution = new LinkedHashMap<>(); int understood = 0; final int openers = 200; for (int i = 0; i < openers; i++) { final int speaker = 100 + (i % 20); final String opener = lines.speak(speaker, "bot_talk", Map.of("personality", "neutral"), Map.of("player", "Kolyan", "level", "35"), false, null); if (opener == null) { continue; } final FakePlayerIntentParser.Intent intent = intents.parse(opener); final String key = (intent != null) ? intent.key : (FakePlayerHeardManager.isQuestionLine(opener) ? "(overheard_q)" : "(overheard)"); distribution.merge(key, 1, Integer::sum); if (intent != null) { understood++; } if (i < 12) { System.out.println(" \"" + opener + "\" -> " + key); } } System.out.println(" intents: " + distribution); check(understood >= (openers / 2), "at least half of bot_talk openers should map to an intent (got " + understood + "/" + openers + ")"); // 4. Sample dialogue as the social layer would run it (opener -> reply seam of the intent). System.out.println("== sample dialogues =="); for (int i = 0; i < 6; i++) { final int a = 300 + i; final int b = 400 + i; final String opener = lines.speak(a, "bot_talk", Map.of("personality", "neutral"), Map.of("player", "Zheka"), false, null); final FakePlayerIntentParser.Intent intent = (opener != null) ? intents.parse(opener) : null; String reply; if ((intent != null) && !intent.seam.isEmpty()) { final Map replySlots = new HashMap<>(slots); replySlots.put("player", "Vovan"); reply = lines.speak(b, intent.seam, Map.of("personality", "helper", "asker", "clanmate"), replySlots, false, FakePlayerIdiolect.Mirror.of(opener)); } else { reply = lines.speak(b, (opener != null) && FakePlayerHeardManager.isQuestionLine(opener) ? "overheard_q" : "overheard", Map.of("personality", "helper"), Map.of("msg", FakePlayerHeardManager.echoOf(opener != null ? opener : ""), "player", "Vovan"), true, null); } System.out.println(" Vovan: " + opener); System.out.println(" Zheka: " + reply + " [" + ((intent != null) ? intent.key : "overheard") + "]"); } // 5. Idiolect: two bots never sound the same. System.out.println("== idiolect =="); final FakePlayerIdiolect one = FakePlayerIdiolect.of(1001); final FakePlayerIdiolect two = FakePlayerIdiolect.of(1002); check(!(one.laugh.equals(two.laugh) && one.filler.equals(two.filler) && (one.chattiness == two.chattiness) && (one.slang == two.slang)), "idiolects should differ"); System.out.println(" 1001: laugh=" + one.laugh + " filler=" + one.filler + " slang=" + one.slang + " mat=" + one.traitMat); System.out.println(" 1002: laugh=" + two.laugh + " filler=" + two.filler + " slang=" + two.slang + " mat=" + two.traitMat); // 6. Heard classification helpers. check(FakePlayerHeardManager.isQuestionLine("кто знает где рб"), "cyrillic question detection"); check(!FakePlayerHeardManager.isQuestionLine("фарм идет норм"), "statement is not a question"); check(FakePlayerHeardManager.isCommercialLine("продам соски дешево"), "commercial detection"); check("продам соски дешево".startsWith(FakePlayerHeardManager.echoOf("продам соски дешево налетай")), "echo takes first words"); // 7. Intents that sieges / economy rely on. final FakePlayerIntentParser.Intent drop = intents.parse("дроп есть?"); check((drop != null) && "ask_drop".equals(drop.key), "ask_drop intent"); final FakePlayerIntentParser.Intent level = intents.parse("какой лвл"); check((level != null) && "ask_level".equals(level.key), "ask_level intent"); final FakePlayerIntentParser.Intent party = intents.parse("го в пати"); check((party != null) && "ask_party".equals(party.key), "ask_party intent"); final FakePlayerIntentParser.Intent how = intents.parse("как фарм?"); check((how != null) && "how_are_you".equals(how.key), "how_are_you intent"); final String[][] regression = { { "го пвп", "duel" }, { "помгите", "ask_help" }, { "дуель?", "duel" }, { "ты бот?", "botcheck" }, { "спс бро", "thanks" }, { "лох", "insult" }, { "где ты", "ask_where" }, { "что нового", "ask_rumors" }, { "пока", "bye" }, { "прив", "greeting" }, { "ок", "yes_ok" }, { "сколько стоит", "ask_price" }, { "клан есть?", "ask_clan" }, { "ты соло?", "ask_solo" }, { "давно тут стоишь?", "ask_long" }, { "пк ходят?", "ask_pk" }, { "здарова, как жизнь молодая?", "how_are_you" }, { "где качаться думаешь", "ask_where" } }; for (String[] pair : regression) { final FakePlayerIntentParser.Intent parsed = intents.parse(pair[0]); final String got = (parsed != null) ? parsed.key : "null"; check(got.equals(pair[1]), "intent of \"" + pair[0] + "\": expected " + pair[1] + ", got " + got); } final String[] mustBeNull = { "думаешь тут фармить?", "молодая", "стоишь тут" }; for (String text : mustBeNull) { final FakePlayerIntentParser.Intent parsed = intents.parse(text); check((parsed == null) || "ask_long".equals(parsed.key) || "ask_where".equals(parsed.key), "no false positive for \"" + text + "\" (got " + ((parsed != null) ? parsed.key : "null") + ")"); } System.out.println("== " + (checks - failures) + "/" + checks + " checks passed =="); System.exit(failures == 0 ? 0 : 1); } }