Files

222 lines
11 KiB
Java

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<String, String> 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<String, String> samples = new TreeMap<>();
for (String seam : codeSeams)
{
String ok = null;
for (int botId = 1; (botId <= 12) && (ok == null); botId++)
{
final Map<String, String> 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<String, String> 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<String, Integer> 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<String, String> 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);
}
}