bunch of small fixes
This commit is contained in:
323
SkyBot/Commands/Fun/Hangman.cs
Normal file
323
SkyBot/Commands/Fun/Hangman.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using SkyBot.Helpers;
|
||||
using SkyBot.Models;
|
||||
using Valour.Sdk.Client;
|
||||
using Valour.Sdk.Models;
|
||||
using Valour.Shared.Authorization;
|
||||
|
||||
namespace SkyBot.Commands
|
||||
{
|
||||
public class Hangman : ICommand
|
||||
{
|
||||
public string Name => "hangman";
|
||||
public string[] Aliases => ["hm"];
|
||||
public string Description => "Starts a channel-wide game of hangman. Optionally specify a category.";
|
||||
public string Section => "Fun";
|
||||
public string Usage => "hangman [category] | hangman end";
|
||||
|
||||
private record HangmanSession(
|
||||
string Word,
|
||||
string? Category,
|
||||
HashSet<char> Guessed,
|
||||
HashSet<char> Wrong,
|
||||
HashSet<string> Contributors,
|
||||
long StarterId,
|
||||
Message BotMessage,
|
||||
Channel Channel,
|
||||
ValourClient Client);
|
||||
|
||||
private static readonly ConcurrentDictionary<long, HangmanSession> _sessions = new();
|
||||
private static readonly HttpClient _http = new();
|
||||
|
||||
private const int MaxWrong = 6;
|
||||
|
||||
private static readonly string[] Topics =
|
||||
[
|
||||
"animals", "food", "sports", "music", "science", "geography",
|
||||
"movies", "nature", "technology", "history", "mythology", "space",
|
||||
"ocean", "weather", "games", "art", "clothing", "vehicles",
|
||||
];
|
||||
|
||||
private static readonly string[] FallbackWords =
|
||||
[
|
||||
"APPLE", "BRIDGE", "CASTLE", "DRAGON", "ELEPHANT", "FOREST", "GUITAR",
|
||||
"HARBOR", "ISLAND", "JUNGLE", "KNIGHT", "LEMON", "MANGO", "OCEAN",
|
||||
"PLANET", "ROBOT", "SNAKE", "TIGER", "UMBRELLA", "WIZARD", "ANCHOR",
|
||||
"BUTTER", "CANDLE", "DONKEY", "ENGINE", "FALCON", "GOBLIN", "HAMMER",
|
||||
"IGLOO", "JACKET", "KITTEN", "LADDER", "MIRROR", "NEEDLE", "ORANGE",
|
||||
"PENCIL", "RABBIT", "SILVER", "TEMPLE", "TURTLE", "VALLEY", "WALRUS",
|
||||
"ZIPPER", "BLANKET", "CACTUS", "DAISY", "GLOVES", "HOCKEY", "INSECT",
|
||||
"JELLY", "KETTLE", "LOBSTER", "MARBLE", "NAPKIN", "OYSTER", "PEPPER",
|
||||
"QUARTZ", "ROCKET", "SALMON", "THRONE", "VELVET", "WINDOW", "YOGURT",
|
||||
"ZOMBIE", "ALMOND", "BISON", "COBRA", "DAGGER", "EMBER", "FROST",
|
||||
"GHOST", "HONEY", "IVORY", "JEWEL", "KOALA", "MAPLE", "NINJA",
|
||||
"OLIVE", "PIRATE", "RAVEN", "SPHINX", "TORNADO", "UNICORN", "VENOM",
|
||||
"WITCH", "PIXEL", "STORM", "CLOUD", "FLAME", "COMET", "DUSK",
|
||||
"ECHO", "FABLE", "GLYPH", "HAZE", "JINX", "KNACK", "LUNAR",
|
||||
"MYTH", "NEON", "ORBIT", "PRISM", "QUEST", "RIDGE", "SHARD",
|
||||
];
|
||||
|
||||
// 7 stages: 0 wrong → 6 wrong (using +--+ to avoid markdown eating underscores)
|
||||
private static readonly string[] Stages =
|
||||
[
|
||||
"```\n +--------+\n | |\n | \n | \n | \n | \n==+==\n```",
|
||||
"```\n +--------+\n | |\n | O\n | \n | \n | \n==+==\n```",
|
||||
"```\n +--------+\n | |\n | O\n | |\n | \n | \n==+==\n```",
|
||||
"```\n +--------+\n | |\n | O\n | /|\n | \n | \n==+==\n```",
|
||||
"```\n +--------+\n | |\n | O\n | /|\\\n | \n | \n==+==\n```",
|
||||
"```\n +--------+\n | |\n | O\n | /|\\\n | / \n | \n==+==\n```",
|
||||
"```\n +--------+\n | |\n | O\n | /|\\\n | / \\\n | \n==+==\n```",
|
||||
];
|
||||
|
||||
private static async Task DeleteBotMessageAsync(CommandContext ctx, Message botMessage)
|
||||
{
|
||||
if (ctx.Client.Cache.Messages.TryGet(botMessage.Id, out var cached) && cached is not null)
|
||||
try { await cached.DeleteAsync(); } catch { }
|
||||
else
|
||||
try { await botMessage.DeleteAsync(); } catch { }
|
||||
}
|
||||
|
||||
private static async Task<Message?> RepostAsync(CommandContext ctx, Channel channel, Message botMessage, string content)
|
||||
{
|
||||
await DeleteBotMessageAsync(ctx, botMessage);
|
||||
var result = await MessageHelper.ReplyAsync(ctx, channel, content);
|
||||
if (!result.Success || result.Data is null) return null;
|
||||
return ctx.Client.Cache.Messages.TryGet(result.Data.Id, out var cached) ? cached : result.Data;
|
||||
}
|
||||
|
||||
private static async Task<string> FetchWord(string topic)
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = $"https://api.datamuse.com/words?ml={Uri.EscapeDataString(topic)}&topic={Uri.EscapeDataString(topic)}&max=500";
|
||||
string json = await _http.GetStringAsync(url);
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var words = doc.RootElement.EnumerateArray()
|
||||
.Select(e => e.GetProperty("word").GetString() ?? "")
|
||||
.Where(w => w.Length >= 4 && w.Length <= 12 && w.All(char.IsLetter))
|
||||
.ToList();
|
||||
|
||||
if (words.Count > 0)
|
||||
return words[Random.Shared.Next(words.Count)].ToUpper();
|
||||
}
|
||||
catch { }
|
||||
|
||||
return FallbackWords[Random.Shared.Next(FallbackWords.Length)];
|
||||
}
|
||||
|
||||
private static string BuildDisplay(string word, string? category, HashSet<char> guessed, HashSet<char> wrong)
|
||||
{
|
||||
string wordDisplay = string.Join(" ", word.Select(c => guessed.Contains(c) ? c.ToString() : "_"));
|
||||
string wrongLetters = wrong.Count > 0 ? string.Join(", ", wrong.OrderBy(c => c)) : "none";
|
||||
string categoryLine = category is not null ? $"📂 **Category**: {category.ToTitleCase()}\n" : "";
|
||||
|
||||
return string.Join("\n",
|
||||
$"🎮 **HANGMAN**",
|
||||
categoryLine,
|
||||
$"`{wordDisplay}`",
|
||||
"",
|
||||
Stages[wrong.Count],
|
||||
"",
|
||||
$"❌ Wrong ({wrong.Count}/{MaxWrong}): {wrongLetters}",
|
||||
"",
|
||||
$"*Use `{Config.Prefix}hg <letter or word>` to guess!*"
|
||||
);
|
||||
}
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
||||
long channelId = ctx.ChannelId;
|
||||
PlanetMember member = ctx.Member;
|
||||
Message message = ctx.Message;
|
||||
string[] args = ctx.Args;
|
||||
|
||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
||||
|
||||
// sd/hangman end — end the current game
|
||||
if (args.Length >= 1 && args[0].ToLower() == "end")
|
||||
{
|
||||
if (!_sessions.TryGetValue(channelId, out var session))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "There's no active hangman game in this channel.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool isStarter = member.UserId == session.StarterId;
|
||||
bool isMod = await PermissionHelper.HasPermAsync(member, channel, [ChatChannelPermissions.ManageMessages]);
|
||||
|
||||
if (!isStarter && !isMod)
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "Only the person who started the game (or a moderator) can end it.");
|
||||
return;
|
||||
}
|
||||
|
||||
_sessions.TryRemove(channelId, out _);
|
||||
await DeleteBotMessageAsync(ctx, session.BotMessage);
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"🛑 Hangman ended by {member.Name}. The word was `{session.Word}`.");
|
||||
return;
|
||||
}
|
||||
|
||||
// sd/hangman [category] — start a new game
|
||||
if (_sessions.ContainsKey(channelId))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "There's already an active hangman game in this channel!");
|
||||
return;
|
||||
}
|
||||
|
||||
string? category = args.Length >= 1
|
||||
? args[0].ToLower()
|
||||
: Topics[Random.Shared.Next(Topics.Length)];
|
||||
|
||||
string word = await FetchWord(category);
|
||||
|
||||
var guessed = new HashSet<char>();
|
||||
var wrong = new HashSet<char>();
|
||||
var contributors = new HashSet<string>();
|
||||
|
||||
string display = BuildDisplay(word, category, guessed, wrong);
|
||||
var sent = await MessageHelper.ReplyAsync(ctx, channel, display);
|
||||
if (!sent.Success || sent.Data is null) return;
|
||||
|
||||
_sessions[channelId] = new HangmanSession(word, category, guessed, wrong, contributors, member.UserId, sent.Data, channel, ctx.Client);
|
||||
}
|
||||
|
||||
public static async Task ProcessGuessAsync(CommandContext ctx, Channel channel, string rawGuess)
|
||||
{
|
||||
long channelId = ctx.ChannelId;
|
||||
|
||||
if (!_sessions.TryGetValue(channelId, out var session))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "There's no active hangman game in this channel.");
|
||||
return;
|
||||
}
|
||||
|
||||
string guess = rawGuess.ToUpper();
|
||||
string memberName = ctx.Member.Name ?? "Unknown";
|
||||
|
||||
// Full word guess
|
||||
if (guess.Length > 1)
|
||||
{
|
||||
if (guess == session.Word)
|
||||
{
|
||||
foreach (char c in session.Word) session.Guessed.Add(c);
|
||||
session.Contributors.Add(memberName);
|
||||
_sessions.TryRemove(channelId, out _);
|
||||
|
||||
string contributorList = string.Join(", ", session.Contributors);
|
||||
await RepostAsync(ctx, channel, session.BotMessage,
|
||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong)
|
||||
+ $"\n\n🎉 **{memberName} guessed the word! The word was `{session.Word}`!**\nContributors: {contributorList}");
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"🎉 **{memberName} guessed the word!** The word was `{session.Word}`!");
|
||||
}
|
||||
else
|
||||
{
|
||||
session.Wrong.Add(guess[0]);
|
||||
|
||||
if (session.Wrong.Count >= MaxWrong)
|
||||
{
|
||||
_sessions.TryRemove(channelId, out _);
|
||||
await RepostAsync(ctx, channel, session.BotMessage,
|
||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong)
|
||||
+ $"\n\n💀 **Game over!** The word was `{session.Word}`.");
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"💀 **Game Over!** Out of guesses. The word was `{session.Word}`");
|
||||
}
|
||||
else
|
||||
{
|
||||
var newMsg = await RepostAsync(ctx, channel, session.BotMessage,
|
||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong));
|
||||
if (newMsg is not null)
|
||||
_sessions[channelId] = session with { BotMessage = newMsg };
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"❌ `{guess}` is not the word!");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Single letter guess
|
||||
char letter = guess[0];
|
||||
|
||||
if (!char.IsLetter(letter))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "Please guess a letter or a full word.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.Guessed.Contains(letter) || session.Wrong.Contains(letter))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"`{letter}` has already been guessed!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.Word.Contains(letter))
|
||||
{
|
||||
session.Guessed.Add(letter);
|
||||
session.Contributors.Add(memberName);
|
||||
|
||||
bool won = session.Word.All(c => session.Guessed.Contains(c));
|
||||
if (won)
|
||||
{
|
||||
_sessions.TryRemove(channelId, out _);
|
||||
string contributorList = string.Join(", ", session.Contributors);
|
||||
await RepostAsync(ctx, channel, session.BotMessage,
|
||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong)
|
||||
+ $"\n\n🎉 **The channel wins! The word was `{session.Word}`!**\nContributors: {contributorList}");
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"🎉 **The channel wins!** The word was `{session.Word}`!");
|
||||
}
|
||||
else
|
||||
{
|
||||
var newMsg = await RepostAsync(ctx, channel, session.BotMessage,
|
||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong));
|
||||
if (newMsg is not null)
|
||||
_sessions[channelId] = session with { BotMessage = newMsg };
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"✅ `{letter}` is in the word!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
session.Wrong.Add(letter);
|
||||
|
||||
if (session.Wrong.Count >= MaxWrong)
|
||||
{
|
||||
_sessions.TryRemove(channelId, out _);
|
||||
await RepostAsync(ctx, channel, session.BotMessage,
|
||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong)
|
||||
+ $"\n\n💀 **Game over!** The word was `{session.Word}`.");
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"💀 **Game over!** The word was `{session.Word}`.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var newMsg = await RepostAsync(ctx, channel, session.BotMessage,
|
||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong));
|
||||
if (newMsg is not null)
|
||||
_sessions[channelId] = session with { BotMessage = newMsg };
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"❌ No `{letter}` in the word!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class HangmanGuess : ICommand
|
||||
{
|
||||
public string Name => "hg";
|
||||
public string[] Aliases => [];
|
||||
public string Description => "Guess a letter or word in the active Hangman game.";
|
||||
public string Section => "Fun";
|
||||
public string Usage => "hg <letter or word>";
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
if (!ctx.ChannelCache.TryGetValue(ctx.ChannelId, out var channel)) return;
|
||||
|
||||
if (ctx.Args.Length == 0 || string.IsNullOrWhiteSpace(ctx.Args[0]))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a letter or word to guess.");
|
||||
return;
|
||||
}
|
||||
|
||||
await Hangman.ProcessGuessAsync(ctx, channel, ctx.Args[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
256
SkyBot/Commands/Fun/Trivia.cs
Normal file
256
SkyBot/Commands/Fun/Trivia.cs
Normal file
@@ -0,0 +1,256 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using SkyBot.Helpers;
|
||||
using SkyBot.Models;
|
||||
using Valour.Sdk.Client;
|
||||
using Valour.Sdk.Models;
|
||||
|
||||
namespace SkyBot.Commands
|
||||
{
|
||||
public class Trivia : ICommand
|
||||
{
|
||||
public string Name => "trivia";
|
||||
public string[] Aliases => ["triv"];
|
||||
public string Description => "Starts a channel-wide trivia question. Everyone has 30 seconds to guess.";
|
||||
public string Section => "Fun";
|
||||
public string Usage => "trivia [easy|medium|hard] [topic] | trivia topics";
|
||||
|
||||
private record GuessEntry(char Letter, string MemberName);
|
||||
|
||||
private record TriviaSession(
|
||||
char CorrectLetter,
|
||||
string QuestionText,
|
||||
List<string> Answers,
|
||||
Message BotMessage,
|
||||
Channel Channel,
|
||||
ValourClient Client,
|
||||
ConcurrentDictionary<long, GuessEntry> Guesses);
|
||||
|
||||
private static readonly ConcurrentDictionary<long, TriviaSession> _sessions = new();
|
||||
private static readonly HttpClient _http = new();
|
||||
|
||||
private static readonly string[] Difficulties = ["easy", "medium", "hard"];
|
||||
|
||||
private static readonly Dictionary<string, int> Categories = new()
|
||||
{
|
||||
["general"] = 9,
|
||||
["books"] = 10,
|
||||
["film"] = 11,
|
||||
["movies"] = 11,
|
||||
["music"] = 12,
|
||||
["musicals"] = 13,
|
||||
["tv"] = 14,
|
||||
["television"] = 14,
|
||||
["games"] = 15,
|
||||
["videogames"] = 15,
|
||||
["boardgames"] = 16,
|
||||
["science"] = 17,
|
||||
["nature"] = 17,
|
||||
["computers"] = 18,
|
||||
["tech"] = 18,
|
||||
["math"] = 19,
|
||||
["maths"] = 19,
|
||||
["mythology"] = 20,
|
||||
["sports"] = 21,
|
||||
["geography"] = 22,
|
||||
["geo"] = 22,
|
||||
["history"] = 23,
|
||||
["politics"] = 24,
|
||||
["art"] = 25,
|
||||
["celebrities"] = 26,
|
||||
["animals"] = 27,
|
||||
["vehicles"] = 28,
|
||||
["cars"] = 28,
|
||||
["comics"] = 29,
|
||||
["anime"] = 31,
|
||||
["manga"] = 31,
|
||||
["cartoons"] = 32,
|
||||
};
|
||||
|
||||
public static async Task ProcessGuessAsync(CommandContext ctx, Channel channel, string rawGuess)
|
||||
{
|
||||
long channelId = ctx.ChannelId;
|
||||
|
||||
if (!_sessions.TryGetValue(channelId, out var session))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "There's no active trivia question in this channel.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.Guesses.ContainsKey(ctx.Message.AuthorUserId))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "You've already submitted an answer!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rawGuess))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a letter. `A, B, C, or D`.");
|
||||
return;
|
||||
}
|
||||
|
||||
char given = char.ToUpper(rawGuess[0]);
|
||||
if (given < 'A' || given > 'D')
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "Invalid choice. Please guess A, B, C, or D.");
|
||||
return;
|
||||
}
|
||||
|
||||
session.Guesses[ctx.Message.AuthorUserId] = new GuessEntry(given, ctx.Member.Name ?? "Unknown");
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "📬 Answer submitted!");
|
||||
}
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
||||
long channelId = ctx.ChannelId;
|
||||
PlanetMember member = ctx.Member;
|
||||
string[] args = ctx.Args;
|
||||
|
||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
||||
|
||||
// sd/trivia topics
|
||||
if (args.Length >= 1 && (args[0].ToLower() == "topics" || args[0].ToLower() == "t"))
|
||||
{
|
||||
string topicList = string.Join(", ", Categories.Keys.Order());
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"**Available topics:** {topicList}\n**Difficulties:** easy, medium, hard");
|
||||
return;
|
||||
}
|
||||
|
||||
// sd/trivia — fetch a new question
|
||||
if (_sessions.ContainsKey(channelId))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "There's already an active trivia question in this channel!");
|
||||
return;
|
||||
}
|
||||
|
||||
string? difficulty = null;
|
||||
int? categoryId = null;
|
||||
|
||||
foreach (string arg in args.Select(a => a.ToLower()))
|
||||
{
|
||||
if (Difficulties.Contains(arg))
|
||||
difficulty = arg;
|
||||
else if (Categories.TryGetValue(arg, out int id))
|
||||
categoryId = id;
|
||||
}
|
||||
|
||||
// Pick a random category if none was specified
|
||||
if (categoryId is null)
|
||||
{
|
||||
var ids = Categories.Values.Distinct().ToArray();
|
||||
categoryId = ids[Random.Shared.Next(ids.Length)];
|
||||
}
|
||||
|
||||
string url = "https://opentdb.com/api.php?amount=1&type=multiple";
|
||||
if (difficulty is not null) url += $"&difficulty={difficulty}";
|
||||
if (categoryId is not null) url += $"&category={categoryId}";
|
||||
|
||||
string rawJson;
|
||||
try
|
||||
{
|
||||
rawJson = await _http.GetStringAsync(url);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "Failed to fetch a trivia question. Try again in a moment.");
|
||||
return;
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(rawJson);
|
||||
var result = doc.RootElement.GetProperty("results")[0];
|
||||
|
||||
string question = WebUtility.HtmlDecode(result.GetProperty("question").GetString()!);
|
||||
string correct = WebUtility.HtmlDecode(result.GetProperty("correct_answer").GetString()!);
|
||||
string category = WebUtility.HtmlDecode(result.GetProperty("category").GetString()!);
|
||||
string fetchedDifficulty = result.GetProperty("difficulty").GetString() ?? "unknown";
|
||||
|
||||
List<string> answers = result.GetProperty("incorrect_answers").EnumerateArray()
|
||||
.Select(x => WebUtility.HtmlDecode(x.GetString()!))
|
||||
.Append(correct)
|
||||
.OrderBy(_ => Random.Shared.Next())
|
||||
.ToList();
|
||||
|
||||
char correctLetter = (char)('A' + answers.IndexOf(correct));
|
||||
|
||||
string questionText = string.Join("\n",
|
||||
$"**Category**: {category} | **Difficulty**: {fetchedDifficulty.ToTitleCase()}",
|
||||
"",
|
||||
$"**{question}**",
|
||||
"",
|
||||
string.Join("\n", answers.Select((a, i) => $"{(char)('A' + i)}) {a}")),
|
||||
"",
|
||||
$"*Use `{Config.Prefix}tg <A/B/C/D>` — you have 30 seconds!*"
|
||||
);
|
||||
|
||||
var sent = await MessageHelper.ReplyAsync(ctx, channel, questionText);
|
||||
if (!sent.Success || sent.Data is null) return;
|
||||
|
||||
Message botMessage = ctx.Client.Cache.Messages.TryGet(sent.Data.Id, out var cachedSent) && cachedSent is not null
|
||||
? cachedSent : sent.Data;
|
||||
|
||||
var newSession = new TriviaSession(correctLetter, questionText, answers, botMessage, channel, ctx.Client, new());
|
||||
_sessions[channelId] = newSession;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(30_000);
|
||||
_sessions.TryRemove(channelId, out _);
|
||||
|
||||
List<string> corrects = [..newSession.Guesses
|
||||
.Where(kv => kv.Value.Letter == newSession.CorrectLetter)
|
||||
.Select(kv => kv.Value.MemberName)];
|
||||
|
||||
List<string> wrongs = [..newSession.Guesses
|
||||
.Where(kv => kv.Value.Letter != newSession.CorrectLetter)
|
||||
.Select(kv => kv.Value.MemberName)];
|
||||
|
||||
string correctAnswer = newSession.Answers[newSession.CorrectLetter - 'A'];
|
||||
string resultsText = $"⏰ **Time's up!** The answer was **{newSession.CorrectLetter}) {correctAnswer}**\n";
|
||||
|
||||
if (corrects.Count > 0)
|
||||
resultsText += $"\n✅ **Correct:** {string.Join(", ", corrects)}";
|
||||
if (wrongs.Count > 0)
|
||||
resultsText += $"\n❌ **Wrong:** {string.Join(", ", wrongs)}";
|
||||
if (newSession.Guesses.IsEmpty)
|
||||
resultsText += "\n😶 Nobody answered!";
|
||||
|
||||
var resultMsg = new Message(newSession.Client)
|
||||
{
|
||||
Content = resultsText,
|
||||
ChannelId = newSession.Channel.Id,
|
||||
PlanetId = newSession.Channel.Planet!.Id,
|
||||
AuthorUserId = newSession.Client.Me.Id,
|
||||
AuthorMemberId = newSession.Channel.Planet?.MyMember.Id,
|
||||
ReplyToId = newSession.BotMessage.Id,
|
||||
Fingerprint = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
await newSession.Client.MessageService.SendMessage(resultMsg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class TriviaGuess : ICommand
|
||||
{
|
||||
public string Name => "tg";
|
||||
public string[] Aliases => [];
|
||||
public string Description => "Submit your answer to the active Trivia question.";
|
||||
public string Section => "Fun";
|
||||
public string Usage => "tg <A/B/C/D>";
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
if (!ctx.ChannelCache.TryGetValue(ctx.ChannelId, out var channel)) return;
|
||||
|
||||
if (ctx.Args.Length == 0 || string.IsNullOrWhiteSpace(ctx.Args[0]))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a letter. `A, B, C, or D`.");
|
||||
return;
|
||||
}
|
||||
|
||||
await Trivia.ProcessGuessAsync(ctx, channel, ctx.Args[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
403
SkyBot/Commands/Fun/Wordle.cs
Normal file
403
SkyBot/Commands/Fun/Wordle.cs
Normal file
@@ -0,0 +1,403 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using SkyBot.Helpers;
|
||||
using SkyBot.Models;
|
||||
using Valour.Sdk.Client;
|
||||
using Valour.Sdk.Models;
|
||||
using Valour.Shared.Authorization;
|
||||
|
||||
namespace SkyBot.Commands
|
||||
{
|
||||
public class Wordle : ICommand
|
||||
{
|
||||
public string Name => "wordle";
|
||||
public string[] Aliases => ["wd"];
|
||||
public string Description => "Starts a channel-wide game of Wordle. Guess the 5-letter word in 6 tries!";
|
||||
public string Section => "Fun";
|
||||
public string Usage => "wordle | wordle board";
|
||||
|
||||
private record WordleSession(
|
||||
string Word,
|
||||
List<string> Guesses,
|
||||
List<string[]> Feedback,
|
||||
HashSet<string> Contributors,
|
||||
long StarterId,
|
||||
Message BotMessage,
|
||||
Channel Channel,
|
||||
ValourClient Client);
|
||||
|
||||
private static readonly ConcurrentDictionary<long, WordleSession> _sessions = new();
|
||||
private static readonly HttpClient _http = new();
|
||||
private static readonly SemaphoreSlim _fetchLock = new(1, 1);
|
||||
private static string[]? _cachedWords;
|
||||
|
||||
private const int MaxGuesses = 6;
|
||||
private const int WordLength = 5;
|
||||
|
||||
private static readonly string[] WordList =
|
||||
[
|
||||
"ABOUT", "ABUSE", "ACUTE", "ADMIT", "ADOPT", "AFTER", "AGENT", "AGREE",
|
||||
"AHEAD", "ALIKE", "ALIVE", "ALONE", "ALONG", "ALTER", "ANGEL", "ANGER",
|
||||
"ANGLE", "APART", "APPLE", "APPLY", "ARGUE", "ARISE", "ARMOR", "ASIDE",
|
||||
"ASSET", "AVOID", "AWARD", "AWARE", "BADLY", "BASIC", "BEACH", "BEARD",
|
||||
"BEGAN", "BEGIN", "BEING", "BELOW", "BENCH", "BLACK", "BLADE", "BLAME",
|
||||
"BLANK", "BLAST", "BLAZE", "BLEED", "BLESS", "BLIND", "BLOCK", "BLOOD",
|
||||
"BLOOM", "BLUNT", "BOARD", "BOOST", "BOUND", "BRAND", "BRAVE", "BREAD",
|
||||
"BREAK", "BREED", "BRICK", "BRIDE", "BRIEF", "BRING", "BROAD", "BROOK",
|
||||
"BROWN", "BRUSH", "BUILD", "BUILT", "BURST", "BUYER", "CABIN", "CABLE",
|
||||
"CANDY", "CARRY", "CHAIN", "CHAIR", "CHAOS", "CHARM", "CHEAP", "CHECK",
|
||||
"CHESS", "CHEST", "CHIEF", "CHILD", "CHILL", "CIVIC", "CIVIL", "CLAIM",
|
||||
"CLASS", "CLEAN", "CLEAR", "CLIMB", "CLOCK", "CLOSE", "CLOUD", "COACH",
|
||||
"COAST", "COUNT", "COURT", "COVER", "CRACK", "CRAFT", "CRANE", "CRAZY",
|
||||
"CREAM", "CREEK", "CRIME", "CROSS", "CROWD", "CRUSH", "CURVE", "CYCLE",
|
||||
"DAILY", "DANCE", "DEATH", "DEBUT", "DENSE", "DEPOT", "DEPTH", "DERBY",
|
||||
"DINER", "DIRTY", "DISCO", "DITCH", "DIZZY", "DOUBT", "DOUGH", "DRAFT",
|
||||
"DRAIN", "DRAMA", "DRANK", "DREAM", "DRESS", "DRIFT", "DRINK", "DRIVE",
|
||||
"DRONE", "DROVE", "DROWN", "DRUNK", "DYING", "EAGER", "EAGLE", "EARLY",
|
||||
"EARTH", "EIGHT", "ELITE", "EMPTY", "ENEMY", "ENJOY", "ENTER", "ENTRY",
|
||||
"EQUAL", "ERROR", "ESSAY", "EVENT", "EVERY", "EXACT", "EXIST", "EXTRA",
|
||||
"FABLE", "FAINT", "FAITH", "FALSE", "FANCY", "FATAL", "FAULT", "FEAST",
|
||||
"FENCE", "FEVER", "FIELD", "FIERY", "FIFTH", "FIFTY", "FIGHT", "FINAL",
|
||||
"FIRST", "FIXED", "FLAME", "FLASH", "FLEET", "FLESH", "FLOAT", "FLOCK",
|
||||
"FLOOD", "FLOOR", "FLUSH", "FOCUS", "FORCE", "FORGE", "FORTH", "FOUND",
|
||||
"FRAME", "FRANK", "FRAUD", "FRESH", "FRONT", "FROST", "FRUIT", "FULLY",
|
||||
"FUNNY", "GIANT", "GIVEN", "GLASS", "GLEAM", "GLOBE", "GLOOM", "GLORY",
|
||||
"GLOVE", "GOING", "GRACE", "GRADE", "GRAIN", "GRAND", "GRANT", "GRAPE",
|
||||
"GRASP", "GRASS", "GRAVE", "GREAT", "GREEN", "GREET", "GRIEF", "GRIND",
|
||||
"GROAN", "GROOM", "GROSS", "GROUP", "GROVE", "GROWN", "GUARD", "GUESS",
|
||||
"GUIDE", "GUILT", "GUISE", "HARSH", "HEART", "HEAVY", "HENCE", "HERBS",
|
||||
"HINGE", "HONEY", "HONOR", "HORSE", "HOTEL", "HOUSE", "HUMAN", "HURRY",
|
||||
"IDEAL", "IMAGE", "INNER", "INPUT", "ISSUE", "IVORY", "JEWEL", "JOINT",
|
||||
"JUDGE", "JUICE", "JUICY", "JUMBO", "KARMA", "KNACK", "KNEEL", "KNIFE",
|
||||
"KNOCK", "KNOWN", "LABEL", "LANCE", "LARGE", "LASER", "LATER", "LAUGH",
|
||||
"LAYER", "LEARN", "LEASE", "LEAST", "LEGAL", "LEMON", "LEVEL", "LIGHT",
|
||||
"LIMIT", "LIVER", "LOCAL", "LODGE", "LOGIC", "LOOSE", "LOVER", "LOWER",
|
||||
"LUCKY", "LUNAR", "MAGIC", "MAJOR", "MAKER", "MANOR", "MAPLE", "MARCH",
|
||||
"MATCH", "MAYOR", "MEDIA", "MERCY", "MERIT", "METAL", "MIGHT", "MINOR",
|
||||
"MINUS", "MODEL", "MONEY", "MONTH", "MORAL", "MOTOR", "MOUNT", "MOUSE",
|
||||
"MOUTH", "MOVIE", "MUSIC", "NAIVE", "NERVE", "NEVER", "NIGHT", "NINJA",
|
||||
"NOBLE", "NOISE", "NORTH", "NOVEL", "NURSE", "NYMPH", "OCEAN", "OFFER",
|
||||
"OFTEN", "OLIVE", "ONSET", "OPERA", "ORDER", "OTHER", "OUTER", "OWNED",
|
||||
"OWNER", "OZONE", "PAINT", "PANIC", "PAPER", "PARTY", "PEACE", "PEACH",
|
||||
"PEARL", "PENNY", "PHASE", "PHONE", "PHOTO", "PIANO", "PIECE", "PILOT",
|
||||
"PIXEL", "PLACE", "PLAIN", "PLANE", "PLANT", "PLATE", "PLAZA", "PLEAD",
|
||||
"PLUMB", "PLUMP", "POINT", "POLAR", "POPPY", "POWER", "PRESS", "PRICE",
|
||||
"PRIDE", "PRIME", "PRINT", "PRIOR", "PRISM", "PROBE", "PROOF", "PROSE",
|
||||
"PROUD", "PROVE", "PULSE", "PUPIL", "QUEEN", "QUERY", "QUEST", "QUEUE",
|
||||
"QUIET", "QUOTA", "QUOTE", "RADAR", "RADIO", "RAISE", "RALLY", "RANGE",
|
||||
"RAPID", "RATIO", "REACH", "READY", "REALM", "REBEL", "REFER", "REIGN",
|
||||
"RELAX", "REPLY", "RIDER", "RIDGE", "RISKY", "RIVER", "ROBIN", "ROBOT",
|
||||
"ROCKY", "ROUGH", "ROUND", "ROUTE", "ROYAL", "RULER", "RURAL", "SADLY",
|
||||
"SAINT", "SALAD", "SAUCE", "SCALE", "SCENE", "SCENT", "SCOUT", "SENSE",
|
||||
"SEVEN", "SHADE", "SHAFT", "SHALL", "SHAME", "SHAPE", "SHARE", "SHARK",
|
||||
"SHARP", "SHEEP", "SHEER", "SHELF", "SHELL", "SHIFT", "SHINE", "SHIRT",
|
||||
"SHOCK", "SHOOT", "SHORT", "SHOUT", "SIEGE", "SIGHT", "SILLY", "SINCE",
|
||||
"SIXTH", "SIXTY", "SKILL", "SKULL", "SLATE", "SLAVE", "SLEEP", "SLICE",
|
||||
"SLIDE", "SLOPE", "SMALL", "SMART", "SMELL", "SMILE", "SMOKE", "SNAKE",
|
||||
"SOLAR", "SOLID", "SOLVE", "SORRY", "SOUND", "SOUTH", "SPACE", "SPARE",
|
||||
"SPARK", "SPEAK", "SPEAR", "SPEED", "SPEND", "SPICE", "SPINE", "SPITE",
|
||||
"SPLIT", "SPOKE", "SPOON", "SPORT", "SPRAY", "SQUAD", "STACK", "STAFF",
|
||||
"STAGE", "STAIN", "STAIR", "STAKE", "STAND", "STARK", "STATE", "STEAM",
|
||||
"STEEL", "STEEP", "STEER", "STERN", "STICK", "STIFF", "STILL", "STOCK",
|
||||
"STOMP", "STONE", "STORE", "STORM", "STORY", "STRAP", "STRAW", "STRAY",
|
||||
"STRIP", "STUCK", "STUDY", "STUFF", "STYLE", "SUGAR", "SUITE", "SUNNY",
|
||||
"SUPER", "SURGE", "SWAMP", "SWEAR", "SWEEP", "SWEET", "SWIFT", "SWIPE",
|
||||
"SWORD", "TABLE", "TASTE", "TEACH", "TEARS", "TEMPT", "TENSE", "TENTH",
|
||||
"THEFT", "THEIR", "THERE", "THICK", "THING", "THINK", "THIRD", "THREE",
|
||||
"THREW", "THROW", "TIGER", "TIGHT", "TIMER", "TIRED", "TITAN", "TITLE",
|
||||
"TOKEN", "TOPIC", "TOTAL", "TOUCH", "TOUGH", "TOWER", "TOXIC", "TRACE",
|
||||
"TRACK", "TRADE", "TRAIL", "TRAIN", "TRAIT", "TRASH", "TREND", "TRIAL",
|
||||
"TRIBE", "TRICK", "TRIED", "TROOP", "TROUT", "TRUCK", "TRULY", "TRUNK",
|
||||
"TRUST", "TRUTH", "TWIST", "ULTRA", "UNCLE", "UNDER", "UNION", "UNITY",
|
||||
"UNTIL", "UPPER", "UPSET", "URBAN", "USAGE", "USHER", "USUAL", "UTTER",
|
||||
"VAGUE", "VALID", "VALUE", "VAPOR", "VAULT", "VIGOR", "VIRAL", "VISIT",
|
||||
"VITAL", "VIVID", "VOCAL", "VOICE", "VOTER", "WAGER", "WATCH", "WATER",
|
||||
"WEARY", "WEAVE", "WEDGE", "WEIRD", "WHERE", "WHILE", "WHITE", "WHOLE",
|
||||
"WIDER", "WITCH", "WOMAN", "WOMEN", "WORLD", "WORRY", "WORSE", "WORST",
|
||||
"WORTH", "WOULD", "WRATH", "WRITE", "WRONG", "YACHT", "YIELD", "YOUNG",
|
||||
"YOUTH", "ZEBRA",
|
||||
];
|
||||
|
||||
private static async Task<string[]> GetWordPoolAsync()
|
||||
{
|
||||
if (_cachedWords is not null) return _cachedWords;
|
||||
|
||||
await _fetchLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_cachedWords is not null) return _cachedWords;
|
||||
|
||||
string json = await _http.GetStringAsync(
|
||||
"https://api.datamuse.com/words?sp=?????&max=1000&md=f");
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var words = doc.RootElement.EnumerateArray()
|
||||
.Select(e => e.GetProperty("word").GetString() ?? "")
|
||||
.Where(w => w.Length == WordLength && w.All(char.IsLetter))
|
||||
.Select(w => w.ToUpper())
|
||||
.ToArray();
|
||||
|
||||
if (words.Length > 0)
|
||||
{
|
||||
_cachedWords = words;
|
||||
return words;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
_fetchLock.Release();
|
||||
}
|
||||
|
||||
return WordList;
|
||||
}
|
||||
|
||||
private static async Task<bool> IsValidWordAsync(string word)
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = await _http.GetStringAsync(
|
||||
$"https://api.datamuse.com/words?sp={word.ToLower()}&max=1");
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var arr = doc.RootElement;
|
||||
if (arr.GetArrayLength() > 0)
|
||||
{
|
||||
string? returned = arr[0].GetProperty("word").GetString();
|
||||
return string.Equals(returned, word, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns per-letter emoji feedback using standard Wordle rules
|
||||
private static string[] GetFeedback(string guess, string word)
|
||||
{
|
||||
var result = new string[WordLength];
|
||||
var wordLeft = word.ToCharArray();
|
||||
var guessLeft = guess.ToCharArray();
|
||||
|
||||
// First pass: greens
|
||||
for (int i = 0; i < WordLength; i++)
|
||||
{
|
||||
if (guessLeft[i] == wordLeft[i])
|
||||
{
|
||||
result[i] = "🟩";
|
||||
wordLeft[i] = '\0';
|
||||
guessLeft[i] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: yellows and grays
|
||||
for (int i = 0; i < WordLength; i++)
|
||||
{
|
||||
if (guessLeft[i] == '\0') continue;
|
||||
|
||||
int idx = Array.IndexOf(wordLeft, guessLeft[i]);
|
||||
if (idx >= 0)
|
||||
{
|
||||
result[i] = "🟨";
|
||||
wordLeft[idx] = '\0';
|
||||
}
|
||||
else
|
||||
{
|
||||
result[i] = "⬛";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string BuildDisplay(WordleSession session)
|
||||
{
|
||||
var rows = new List<string>();
|
||||
|
||||
for (int i = 0; i < MaxGuesses; i++)
|
||||
{
|
||||
if (i < session.Guesses.Count)
|
||||
{
|
||||
string emojis = string.Join("", session.Feedback[i]);
|
||||
string letters = string.Join(" ", session.Guesses[i].ToCharArray());
|
||||
rows.Add($"{emojis} {letters}");
|
||||
}
|
||||
else
|
||||
{
|
||||
rows.Add("⬜⬜⬜⬜⬜");
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("\n",
|
||||
"🟩 **WORDLE** — Guess the 5-letter word!",
|
||||
"",
|
||||
string.Join("\n", rows),
|
||||
"",
|
||||
$"Guesses: {session.Guesses.Count}/{MaxGuesses}",
|
||||
"",
|
||||
$"*Use `{Config.Prefix}wg <word>` to guess!*"
|
||||
);
|
||||
}
|
||||
|
||||
public static async Task ProcessGuessAsync(CommandContext ctx, Channel channel, string rawGuess)
|
||||
{
|
||||
long channelId = ctx.ChannelId;
|
||||
if (!_sessions.TryGetValue(channelId, out var session)) return;
|
||||
|
||||
string guess = rawGuess.ToUpper();
|
||||
string memberName = ctx.Member.Name ?? "Unknown";
|
||||
|
||||
if (guess.Length != WordLength || !guess.All(char.IsLetter))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"Your guess must be exactly {WordLength} letters with no numbers or symbols.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await IsValidWordAsync(guess))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"`{guess}` isn't a valid word!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.Guesses.Contains(guess))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"`{guess}` has already been guessed!");
|
||||
return;
|
||||
}
|
||||
|
||||
var feedback = GetFeedback(guess, session.Word);
|
||||
session.Guesses.Add(guess);
|
||||
session.Feedback.Add(feedback);
|
||||
session.Contributors.Add(memberName);
|
||||
|
||||
bool won = guess == session.Word;
|
||||
bool lost = !won && session.Guesses.Count >= MaxGuesses;
|
||||
|
||||
if (won)
|
||||
{
|
||||
_sessions.TryRemove(channelId, out _);
|
||||
string contributorList = string.Join(", ", session.Contributors);
|
||||
await RepostBoardAsync(ctx, channel, session,
|
||||
BuildDisplay(session)
|
||||
+ $"\n\n🎉 **{memberName} got it in {session.Guesses.Count}!**\nContributors: {contributorList}");
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"🎉 **The word was `{session.Word}`!** Got it in {session.Guesses.Count}/{MaxGuesses}!");
|
||||
}
|
||||
else if (lost)
|
||||
{
|
||||
_sessions.TryRemove(channelId, out _);
|
||||
await RepostBoardAsync(ctx, channel, session,
|
||||
BuildDisplay(session)
|
||||
+ $"\n\n💀 **Game over!** The word was `{session.Word}`.");
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"💀 **Game over!** The word was `{session.Word}`.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var newMsg = await RepostBoardAsync(ctx, channel, session, BuildDisplay(session));
|
||||
if (newMsg is not null)
|
||||
_sessions[channelId] = session with { BotMessage = newMsg };
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Message?> RepostBoardAsync(CommandContext ctx, Channel channel, WordleSession session, string content)
|
||||
{
|
||||
if (ctx.Client.Cache.Messages.TryGet(session.BotMessage.Id, out var old) && old is not null)
|
||||
try { await old.DeleteAsync(); } catch { }
|
||||
else
|
||||
try { await session.BotMessage.DeleteAsync(); } catch { }
|
||||
|
||||
var result = await MessageHelper.ReplyAsync(ctx, channel, content);
|
||||
if (!result.Success || result.Data is null) return null;
|
||||
return ctx.Client.Cache.Messages.TryGet(result.Data.Id, out var cached) && cached is not null ? cached : result.Data;
|
||||
}
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
||||
long channelId = ctx.ChannelId;
|
||||
string[] args = ctx.Args;
|
||||
PlanetMember member = ctx.Member;
|
||||
|
||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
||||
|
||||
// sd/wordle end — end the current game
|
||||
if (args.Length >= 1 && args[0].ToLower() == "end")
|
||||
{
|
||||
if (!_sessions.TryGetValue(channelId, out var session))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "There's no active Wordle game in this channel.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool isStarter = member.UserId == session.StarterId;
|
||||
bool isMod = await PermissionHelper.HasPermAsync(member, channel, [ChatChannelPermissions.ManageMessages]);
|
||||
|
||||
if (!isStarter && !isMod)
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "Only the person who started the game (or a moderator) can end it.");
|
||||
return;
|
||||
}
|
||||
|
||||
_sessions.TryRemove(channelId, out _);
|
||||
if (ctx.Client.Cache.Messages.TryGet(session.BotMessage.Id, out var old) && old is not null)
|
||||
try { await old.DeleteAsync(); } catch { }
|
||||
else
|
||||
try { await session.BotMessage.DeleteAsync(); } catch { }
|
||||
await MessageHelper.ReplyAsync(ctx, channel, $"🛑 Wordle ended by {member.Name}. The word was `{session.Word}`.");
|
||||
return;
|
||||
}
|
||||
|
||||
// sd/wordle board — repost the current board
|
||||
if (args.Length >= 1 && args[0].ToLower() == "board")
|
||||
{
|
||||
if (!_sessions.TryGetValue(channelId, out var session))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "There's no active Wordle game in this channel.");
|
||||
return;
|
||||
}
|
||||
|
||||
var newMsg = await RepostBoardAsync(ctx, channel, session, BuildDisplay(session));
|
||||
if (newMsg is not null)
|
||||
_sessions[channelId] = session with { BotMessage = newMsg };
|
||||
return;
|
||||
}
|
||||
|
||||
// sd/wordle — start a new game
|
||||
if (_sessions.ContainsKey(channelId))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "There's already an active Wordle game in this channel!");
|
||||
return;
|
||||
}
|
||||
|
||||
var pool = await GetWordPoolAsync();
|
||||
string word = pool[Random.Shared.Next(pool.Length)];
|
||||
|
||||
var newSession = new WordleSession(word, [], [], [], member.UserId, null!, channel, ctx.Client);
|
||||
string display = BuildDisplay(newSession);
|
||||
var sent = await MessageHelper.ReplyAsync(ctx, channel, display);
|
||||
if (!sent.Success || sent.Data is null) return;
|
||||
|
||||
_sessions[channelId] = newSession with { BotMessage = sent.Data };
|
||||
}
|
||||
}
|
||||
|
||||
public class WordleGuess : ICommand
|
||||
{
|
||||
public string Name => "wg";
|
||||
public string[] Aliases => [];
|
||||
public string Description => "Guess a word in the active Wordle game.";
|
||||
public string Section => "Fun";
|
||||
public string Usage => "wg <word>";
|
||||
|
||||
public async Task Execute(CommandContext ctx)
|
||||
{
|
||||
if (!ctx.ChannelCache.TryGetValue(ctx.ChannelId, out var channel)) return;
|
||||
|
||||
if (ctx.Args.Length == 0 || string.IsNullOrWhiteSpace(ctx.Args[0]))
|
||||
{
|
||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a word to guess.");
|
||||
return;
|
||||
}
|
||||
|
||||
await Wordle.ProcessGuessAsync(ctx, channel, ctx.Args[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace SkyBot.Commands
|
||||
{
|
||||
public string Name => "suggest";
|
||||
public string[] Aliases => [];
|
||||
public string Description => "Shows the source code for this bot.";
|
||||
public string Description => "Sends a link to where you can suggest commands.";
|
||||
public string Section => "Info";
|
||||
public string Usage => "source";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user