Rewrite the !dice command from scratch

This commit is contained in:
runebaas 2020-06-21 03:33:05 +02:00
parent d7e313c9fa
commit 6d44960867
No known key found for this signature in database
GPG key ID: 2677AF508D0300D6
13 changed files with 376 additions and 126 deletions

View file

@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Discord.Commands;
using Geekbot.net.Lib.DiceParser;
using Geekbot.net.Lib.ErrorHandling;
namespace Geekbot.net.Commands.Utils
{
public class Dice : ModuleBase
{
private readonly IErrorHandler _errorHandler;
private readonly IDiceParser _diceParser;
public Dice(IErrorHandler errorHandler, IDiceParser diceParser)
{
_errorHandler = errorHandler;
_diceParser = diceParser;
}
// ToDo: Separate representation and logic
// ToDo: Translate
[Command("dice", RunMode = RunMode.Async)]
[Summary("Roll a dice. (use '!dice help' for a manual)")]
public async Task RollCommand([Remainder] [Summary("input")] string diceInput = "1d20")
{
try
{
if (diceInput == "help")
{
await ShowDiceHelp();
return;
}
var parsed = _diceParser.Parse(diceInput);
var sb = new StringBuilder();
sb.AppendLine($"{Context.User.Mention} :game_die:");
foreach (var die in parsed.Dice)
{
sb.AppendLine($"**{die.DiceName}**");
var diceResultList = new List<string>();
var total = 0;
foreach (var roll in die.Roll())
{
diceResultList.Add(roll.ToString());
total += roll.Result;
}
sb.AppendLine(string.Join(" | ", diceResultList));
if (parsed.SkillModifier != 0)
{
sb.AppendLine($"Skill: {parsed.SkillModifier}");
}
if (parsed.Options.ShowTotal)
{
var totalLine = $"Total: {total}";
if (parsed.SkillModifier > 0)
{
totalLine += ($" (+{parsed.SkillModifier} = {total + parsed.SkillModifier})");
}
if (parsed.SkillModifier < 0)
{
totalLine += ($" ({parsed.SkillModifier} = {total - parsed.SkillModifier})");
}
sb.AppendLine(totalLine);
}
}
await Context.Channel.SendMessageAsync(sb.ToString());
}
catch (DiceException e)
{
await Context.Channel.SendMessageAsync($"**:warning: {e.DiceName} is invalid:** {e.Message}");
}
catch (Exception e)
{
await _errorHandler.HandleCommandException(e, Context);
}
}
private async Task ShowDiceHelp()
{
var sb = new StringBuilder();
sb.AppendLine("**__Examples__**");
sb.AppendLine("```");
sb.AppendLine("'!dice' - throw a 1d20");
sb.AppendLine("'!dice 1d12' - throw a 1d12");
sb.AppendLine("'!dice +1d20' - throw with advantage");
sb.AppendLine("'!dice -1d20' - throw with disadvantage");
sb.AppendLine("'!dice 1d20 +2' - throw with a +2 skill bonus");
sb.AppendLine("'!dice 1d20 -2' - throw with a -2 skill bonus");
sb.AppendLine("'!dice 8d6' - throw ~~a fireball~~ a 8d6");
sb.AppendLine("'!dice 8d6 total' - calculate the total");
sb.AppendLine("'!dice 2d20 6d6 2d4 2d12' - drop your dice pouch");
sb.AppendLine("```");
await Context.Channel.SendMessageAsync(sb.ToString());
}
}
}

View file

@ -1,116 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Discord.Commands;
using Geekbot.net.Lib.RandomNumberGenerator;
namespace Geekbot.net.Commands.Utils.Dice
{
public class Dice : ModuleBase
{
private readonly IRandomNumberGenerator _randomNumberGenerator;
public Dice(IRandomNumberGenerator randomNumberGenerator)
{
_randomNumberGenerator = randomNumberGenerator;
}
[Command("dice", RunMode = RunMode.Async)]
[Summary("Roll a dice.")]
public async Task RollCommand([Remainder] [Summary("dice-type")] string diceType = "1d20")
{
var splitedDices = diceType.Split("+");
var dices = new List<DiceTypeDto>();
var mod = 0;
foreach (var i in splitedDices)
{
var dice = ToDice(i);
if (dice.Sides != 0 && dice.Times != 0)
{
dices.Add(dice);
}
else if (dice.Mod != 0)
{
if (mod != 0)
{
await ReplyAsync("You can only have one mod");
return;
}
mod = dice.Mod;
}
}
if (!dices.Any())
{
await ReplyAsync(
"That is not a valid dice, examples are: 1d20, 1d6, 2d6, 1d6+2, 1d6+2d8+1d20+6, etc...");
return;
}
if (dices.Any(d => d.Times > 20))
{
await ReplyAsync("You can't throw more than 20 dices");
return;
}
if (dices.Any(d => d.Sides > 144))
{
await ReplyAsync("A dice can't have more than 144 sides");
return;
}
var rep = new StringBuilder();
rep.AppendLine($":game_die: {Context.User.Mention}");
rep.Append("**Result:** ");
var resultStrings = new List<string>();
var total = 0;
foreach (var dice in dices)
{
var results = new List<int>();
for (var i = 0; i < dice.Times; i++)
{
var roll = _randomNumberGenerator.Next(1, dice.Sides);
total += roll;
results.Add(roll);
}
resultStrings.Add($"{dice.DiceType} ({string.Join(",", results)})");
}
rep.Append(string.Join(" + ", resultStrings));
if (mod != 0)
{
rep.Append($" + {mod}");
total += mod;
}
rep.AppendLine();
rep.AppendLine($"**Total:** {total}");
await ReplyAsync(rep.ToString());
}
private DiceTypeDto ToDice(string dice)
{
var diceParts = dice.Split('d');
if (diceParts.Length == 2
&& int.TryParse(diceParts[0], out var times)
&& int.TryParse(diceParts[1], out var max))
return new DiceTypeDto
{
DiceType = dice,
Times = times,
Sides = max
};
if (dice.Length == 1
&& int.TryParse(diceParts[0], out var mod))
return new DiceTypeDto
{
Mod = mod
};
return new DiceTypeDto();
}
}
}

View file

@ -1,10 +0,0 @@
namespace Geekbot.net.Commands.Utils.Dice
{
internal class DiceTypeDto
{
public string DiceType { get; set; }
public int Times { get; set; }
public int Sides { get; set; }
public int Mod { get; set; }
}
}

View file

@ -0,0 +1,13 @@
using System;
namespace Geekbot.net.Lib.DiceParser
{
public class DiceException : Exception
{
public DiceException(string message) : base(message)
{
}
public string DiceName { get; set; }
}
}

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Geekbot.net.Lib.DiceParser
{
public class DiceInput
{
public List<SingleDie> Dice { get; set; } = new List<SingleDie>();
public DiceInputOptions Options { get; set; } = new DiceInputOptions();
public int SkillModifier { get; set; }
}
}

View file

@ -0,0 +1,7 @@
namespace Geekbot.net.Lib.DiceParser
{
public struct DiceInputOptions
{
public bool ShowTotal { get; set; }
}
}

View file

@ -0,0 +1,102 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Geekbot.net.Lib.RandomNumberGenerator;
namespace Geekbot.net.Lib.DiceParser
{
public class DiceParser : IDiceParser
{
private readonly IRandomNumberGenerator _randomNumberGenerator;
private readonly Regex _inputRegex;
private readonly Regex _singleDieRegex;
public DiceParser(IRandomNumberGenerator randomNumberGenerator)
{
_randomNumberGenerator = randomNumberGenerator;
_inputRegex = new Regex(
@"((?<DieAdvantage>\+\d+d\d+)|(?<DieDisadvantage>\-\d+d\d+)|(?<DieNormal>\d+d\d+)|(?<Keywords>(total))|(?<SkillModifer>(\+|\-)\d+))\s",
RegexOptions.Compiled | RegexOptions.IgnoreCase,
new TimeSpan(0, 0, 2));
_singleDieRegex = new Regex(
@"\d+d\d+",
RegexOptions.Compiled | RegexOptions.IgnoreCase,
new TimeSpan(0, 0, 0, 0, 500));
}
public DiceInput Parse(string input)
{
// adding a whitespace at the end, otherwise the parser might pickup on false items
var inputWithExtraWhitespace = $"{input} ";
var matches = _inputRegex.Matches(inputWithExtraWhitespace);
var result = new DiceInput();
var resultOptions = new DiceInputOptions();
foreach (Match match in matches)
{
foreach (Group matchGroup in match.Groups)
{
if (matchGroup.Success)
{
switch (matchGroup.Name)
{
case "DieNormal":
result.Dice.Add(Die(matchGroup.Value, DieAdvantageType.None));
break;
case "DieAdvantage":
result.Dice.Add(Die(matchGroup.Value, DieAdvantageType.Advantage));
break;
case "DieDisadvantage":
result.Dice.Add(Die(matchGroup.Value, DieAdvantageType.Disadvantage));
break;
case "Keywords":
Keywords(matchGroup.Value, ref resultOptions);
break;
case "SkillModifer":
result.SkillModifier = SkillModifer(matchGroup.Value);
break;
}
}
}
}
if (!result.Dice.Any())
{
result.Dice.Add(new SingleDie(_randomNumberGenerator));
}
result.Options = resultOptions;
return result;
}
private SingleDie Die(string match, DieAdvantageType advantageType)
{
var x = _singleDieRegex.Match(match).Value.Split('d');
var die = new SingleDie(_randomNumberGenerator)
{
Amount = int.Parse(x[0]),
Sides = int.Parse(x[1]),
AdvantageType = advantageType
};
die.ValidateDie();
return die;
}
private int SkillModifer(string match)
{
return int.Parse(match);
}
private void Keywords(string match, ref DiceInputOptions options)
{
switch (match)
{
case "total":
options.ShowTotal = true;
break;
}
}
}
}

View file

@ -0,0 +1,9 @@
namespace Geekbot.net.Lib.DiceParser
{
public enum DieAdvantageType
{
Advantage,
Disadvantage,
None
}
}

View file

@ -0,0 +1,30 @@
using System;
namespace Geekbot.net.Lib.DiceParser
{
public class DieResult
{
// public int Result { get; set; }
public int Roll1 { get; set; }
public int Roll2 { get; set; }
public DieAdvantageType AdvantageType { get; set; }
public override string ToString()
{
return AdvantageType switch
{
DieAdvantageType.Advantage => Roll1 > Roll2 ? $"(**{Roll1}**, {Roll2})" : $"({Roll1}, **{Roll2}**)",
DieAdvantageType.Disadvantage => Roll1 < Roll2 ? $"(**{Roll1}**, {Roll2})" : $"({Roll1}, **{Roll2}**)",
_ => Result.ToString()
};
}
public int Result => AdvantageType switch
{
DieAdvantageType.None => Roll1,
DieAdvantageType.Advantage => Math.Max(Roll1, Roll2),
DieAdvantageType.Disadvantage => Math.Min(Roll1, Roll2),
_ => 0
};
}
}

View file

@ -0,0 +1,7 @@
namespace Geekbot.net.Lib.DiceParser
{
public interface IDiceParser
{
DiceInput Parse(string input);
}
}

View file

@ -0,0 +1,72 @@
using System.Collections.Generic;
using Geekbot.net.Lib.Extensions;
using Geekbot.net.Lib.RandomNumberGenerator;
namespace Geekbot.net.Lib.DiceParser
{
public class SingleDie
{
private readonly IRandomNumberGenerator _random;
public SingleDie(IRandomNumberGenerator random)
{
_random = random;
}
public int Sides { get; set; } = 20;
public int Amount { get; set; } = 1;
public DieAdvantageType AdvantageType { get; set; } = DieAdvantageType.None;
public string DiceName => AdvantageType switch
{
DieAdvantageType.Advantage => $"{Amount}d{Sides} (with advantage)",
DieAdvantageType.Disadvantage => $"{Amount}d{Sides} (with disadvantage)",
_ => $"{Amount}d{Sides}"
};
public List<DieResult> Roll()
{
var results = new List<DieResult>();
Amount.Times(() =>
{
var result = new DieResult
{
Roll1 = _random.Next(1, Sides),
AdvantageType = AdvantageType
};
if (AdvantageType == DieAdvantageType.Advantage || AdvantageType == DieAdvantageType.Disadvantage)
{
result.Roll2 = _random.Next(1, Sides);
}
results.Add(result);
});
return results;
}
public void ValidateDie()
{
if (Amount < 1)
{
throw new DiceException("To few dice, must be a minimum of 1");
}
if (Amount > 24)
{
throw new DiceException("To many dice, maximum allowed is 24") { DiceName = DiceName };
}
if (Sides < 2)
{
throw new DiceException("Die must have at least 2 sides") { DiceName = DiceName };
}
if (Sides > 144)
{
throw new DiceException("Die can not have more than 144 sides") { DiceName = DiceName };
}
}
}
}

View file

@ -0,0 +1,15 @@
using System;
namespace Geekbot.net.Lib.Extensions
{
public static class IntExtensions
{
public static void Times(this int count, Action action)
{
for (var i = 0; i < count; i++)
{
action();
}
}
}
}

View file

@ -11,6 +11,7 @@ using Geekbot.net.Handlers;
using Geekbot.net.Lib; using Geekbot.net.Lib;
using Geekbot.net.Lib.Clients; using Geekbot.net.Lib.Clients;
using Geekbot.net.Lib.Converters; using Geekbot.net.Lib.Converters;
using Geekbot.net.Lib.DiceParser;
using Geekbot.net.Lib.ErrorHandling; using Geekbot.net.Lib.ErrorHandling;
using Geekbot.net.Lib.GlobalSettings; using Geekbot.net.Lib.GlobalSettings;
using Geekbot.net.Lib.GuildSettingsManager; using Geekbot.net.Lib.GuildSettingsManager;
@ -169,6 +170,7 @@ namespace Geekbot.net
var kvMemoryStore = new KvInInMemoryStore(); var kvMemoryStore = new KvInInMemoryStore();
var translationHandler = new TranslationHandler(_logger, _guildSettingsManager); var translationHandler = new TranslationHandler(_logger, _guildSettingsManager);
var errorHandler = new ErrorHandler(_logger, translationHandler, _runParameters); var errorHandler = new ErrorHandler(_logger, translationHandler, _runParameters);
var diceParser = new DiceParser(randomNumberGenerator);
services.AddSingleton(_userRepository); services.AddSingleton(_userRepository);
services.AddSingleton<IGeekbotLogger>(_logger); services.AddSingleton<IGeekbotLogger>(_logger);
@ -183,6 +185,7 @@ namespace Geekbot.net
services.AddSingleton<IKvInMemoryStore>(kvMemoryStore); services.AddSingleton<IKvInMemoryStore>(kvMemoryStore);
services.AddSingleton<IGlobalSettings>(_globalSettings); services.AddSingleton<IGlobalSettings>(_globalSettings);
services.AddSingleton<IErrorHandler>(errorHandler); services.AddSingleton<IErrorHandler>(errorHandler);
services.AddSingleton<IDiceParser>(diceParser);
services.AddSingleton<ITranslationHandler>(translationHandler); services.AddSingleton<ITranslationHandler>(translationHandler);
services.AddSingleton<IReactionListener>(_reactionListener); services.AddSingleton<IReactionListener>(_reactionListener);
services.AddSingleton<IGuildSettingsManager>(_guildSettingsManager); services.AddSingleton<IGuildSettingsManager>(_guildSettingsManager);