diff --git a/DiscordBot/Data/FuzzTable.cs b/DiscordBot/Data/FuzzTable.cs index 92b19024..05e0bc6a 100644 --- a/DiscordBot/Data/FuzzTable.cs +++ b/DiscordBot/Data/FuzzTable.cs @@ -1,28 +1,114 @@ // FuzzTable.cs // using System; +using System.IO; +using System.Collections.Generic; using System.Text.RegularExpressions; namespace DiscordBot.Data; +// A simple random string picking engine. +// +// Load it up like a list with string choices that can be picked from. +// Remember recently-picked choices so they don't repeat too soon. +// Individual string choices can also be evaluated with a simple syntax +// to further allow for alternative wordings, such as humanized messages. +// +// Evaluating "(He|She|They) (picked|chose) a (green|red|blue) (ball|block)." +// might return "She picked a red ball." +// public class FuzzTable { private static Random random = new(); private static Regex parenContents = null; private static TimeSpan timeout = new(10*10000/*x10nanoseconds*/); - //TODO: an instance keeps an array of alternates and an MRU list + private List choices = new(); + private Queue recent = new(); + + public void Clear() + { + choices.Clear(); + recent.Clear(); + } + + public int Count => choices.Count + recent.Count; + + // Add a string as a valid choice from which to pick. + // Note that empty strings or whitespace can be added manually as valid choices. + // Duplicate choices are also allowed for weighting. + // + public void Add(string choice) + { + choices.Add(choice); + } + + // Add a collection of choice strings all at once. + // + public void Add(IEnumerable stream) + { + if (stream == null) + return; + foreach (var choice in stream) + Add(choice); + } + // Load a file of string choices. + // Lines starting with a '#' character are ignored, as are blank lines. + // Each remaining line of the file is trimmed of leading and trailing whitespace. + // Each line is added as a new choice, and duplicates are allowed for weighting. + // + public void Load(string filename) + { + foreach (string line in File.ReadLines(filename)) + { + string choice = line.Trim(); + if (choice.StartsWith('#')) + continue; + Add(choice); + } + } + + // Pick one of the active choices. + // This choice is transferred to the MRU so it's not picked again too soon. + // If the evaluate flag is given, further Evaluate() it as a fuzz string. + // Returns the chosen results, or the empty string if no choices available. + // + public string Pick(bool evaluate=false) + { + Recycle(); + if (choices.Count == 0) + return ""; + int pick = random.Next(0, choices.Count); + string chosen = choices[pick]; + choices.RemoveAt(pick); + recent.Enqueue(chosen); + if (evaluate) + return Evaluate(chosen); + return chosen; + } + + // When the MRU gets too long, return the oldest MRU choice(s) back + // to the active list of choices. + // + private void Recycle() + { + // Caps the MRU at half of total choices. + while (recent.Count > choices.Count) + { + string choice = recent.Dequeue(); + choices.Add(choice); + } + } + // Evaluate a single fuzz string. - // "(He|She|They) (picked|selected) a (green|red|blue) (ball|block)." - // "She picked a red ball." // Replace any parenthetical phrase with one of its choices at random. // Allows for nesting of choices. There's currently no way to escape // parentheses or vertical bars so strings must not include strays. // Returns one permutation from all choice alternatives given. - // Does not remember what choices were given. + // There is no MRU of individual permutations given. // - public static string Evaluate(string fuzz) + public static string Evaluate(string fuzz) { if (string.IsNullOrEmpty(fuzz)) return ""; @@ -61,3 +147,4 @@ private static string PickAlternate(string fuzz) } } + diff --git a/DiscordBot/Modules/UserModule.cs b/DiscordBot/Modules/UserModule.cs index 9fe2994e..492a7410 100644 --- a/DiscordBot/Modules/UserModule.cs +++ b/DiscordBot/Modules/UserModule.cs @@ -9,6 +9,7 @@ using DiscordBot.Utils; using HtmlAgilityPack; using DiscordBot.Attributes; +using DiscordBot.Data; namespace DiscordBot.Modules; @@ -31,6 +32,8 @@ public class UserModule : ModuleBase #endregion private readonly Random _random = new(); + private FuzzTable _slapObjects = new(); + private FuzzTable _slapFails = new(); [Command("Help"), Priority(100)] [Summary("Does what you see now.")] @@ -624,10 +627,20 @@ public async Task SlapUser(params IUser[] users) var uname = Context.User.GetUserPreferredName(); - if (Settings.UserModuleSlapChoices == null || Settings.UserModuleSlapChoices.Count == 0) - Settings.UserModuleSlapChoices = new List() { "fish", "mallet" }; - if (Settings.UserModuleSlapFails == null || Settings.UserModuleSlapFails.Count == 0) - Settings.UserModuleSlapFails = new List() { "hurting themselves" }; + if (_slapObjects.Count == 0) + _slapObjects.Add(Settings.UserModuleSlapChoices); + if (_slapObjects.Count == 0) + _slapObjects.Add("fish|mallet"); + + if (_slapFails.Count == 0) + _slapFails.Add(Settings.UserModuleSlapFails); + if (_slapFails.Count == 0) + _slapFails.Add("hurting themselves"); + + // if (Settings.UserModuleSlapChoices == null || Settings.UserModuleSlapChoices.Count == 0) + // Settings.UserModuleSlapChoices = new List() { "fish", "mallet" }; + // if (Settings.UserModuleSlapFails == null || Settings.UserModuleSlapFails.Count == 0) + // Settings.UserModuleSlapFails = new List() { "hurting themselves" }; bool fail = (_random.Next(1, 100) < 5); @@ -642,9 +655,11 @@ public async Task SlapUser(params IUser[] users) if (fail) { sb.Append(" around a bit with a large "); - sb.Append(Settings.UserModuleSlapChoices[_random.Next() % Settings.UserModuleSlapChoices.Count]); + sb.Append(_slapObjects.Pick(true)); + //sb.Append(Settings.UserModuleSlapChoices[_random.Next() % Settings.UserModuleSlapChoices.Count]); sb.Append(", but misses and ends up "); - sb.Append(Settings.UserModuleSlapFails[_random.Next() % Settings.UserModuleSlapFails.Count]); + sb.Append(_slapFails.Pick(true)); + //sb.Append(Settings.UserModuleSlapFails[_random.Next() % Settings.UserModuleSlapFails.Count]); sb.Append("."); } else