From d4e7170be12838da0e8c186c1d9e2b6310cb47e4 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Tue, 11 Jan 2022 13:15:25 +0700 Subject: [PATCH] Use classes for actions, damage calculations, random weapons, Status command --- Program.fs | 395 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 259 insertions(+), 136 deletions(-) diff --git a/Program.fs b/Program.fs index 5ad1f9a..8d22152 100644 --- a/Program.fs +++ b/Program.fs @@ -6,21 +6,65 @@ open DSharpPlus.EventArgs open DSharpPlus.SlashCommands open Emzi0767.Utilities -type Hack = - | Virus = 0 - | Ransom = 1 - | DDos = 2 - | Worm = 3 - | Crack = 4 - | Injection = 5 +type ActionClass = + | Network + | Exploit + | Penetration + +type IClass = abstract GetClass : unit -> ActionClass + +type Weapon = + | Virus + | Ransom + | Worm + | DDos + | Crack + | Injection + interface IClass with + member this.GetClass () = + match this with + | Virus | Ransom -> Exploit + | DDos | Worm -> Network + | Crack | Injection -> Penetration + static member TryParse weapon = + match weapon with + | "Virus" -> Some Virus + | "Ransom" -> Some Ransom + | "Worm" -> Some Worm + | "DDos" -> Some DDos + | "Crack" -> Some Crack + | "Injection" -> Some Injection + | _ -> None + + + +type Shield = + | Firewall + | PortScan + | Encryption + | Cypher + | Hardening + | Sanitation + interface IClass with + member this.GetClass () = + match this with + | Firewall | PortScan -> Exploit + | Encryption | Cypher -> Network + | Hardening | Sanitation -> Penetration + static member TryParse shield = + match shield with + | "Firewall" -> Some Firewall + | "PortScan" -> Some PortScan + | "Encryption" -> Some Encryption + | "Cypher" -> Some Cypher + | "Hardening" -> Some Hardening + | "Sanitation" -> Some Sanitation + | _ -> None + +type HackResult = + | Strong + | Weak -type Protection = - | Firewall = 0 - | PortScan = 1 - | Encryption = 2 - | Cypher = 3 - | Hardening = 4 - | Sanitation = 5 type DiscordPlayer = { Id : uint64 @@ -28,44 +72,47 @@ type DiscordPlayer = { } type Attack = { - HackType : Hack + HackType : Weapon Target : DiscordPlayer Timestamp : DateTime } type Defense = { - DefenseType : Protection + DefenseType : Shield Timestamp : DateTime } type Player = { DiscordId : uint64 - Hacks : Hack list - Protections : Protection list + Weapons : Weapon list + Shields : Shield list Attacks : Attack list Defenses : Defense list - Bank : int64 + Bank : single } let mutable players : Player list = [] -type EmptyGlobalCommandToAvoidFamousDuplicateSlashCommandsBug() = inherit ApplicationCommandModule () - let newPlayer (membr : uint64) = -// let rand = System.Random(System.Guid.NewGuid().GetHashCode()) -// let hacks = -// [0..2] -// |> Set.map (fun _ -> enum(rand.Next(0, 6))) -// let defns = -// [0..2] -// |> Set.map (fun _ -> enum(rand.Next(0, 6))) + let h1 = [| Virus ; Ransom |] + let h2 = [| DDos ; Worm |] + let h3 = [| Crack ; Injection |] + let d1 = [| Firewall ; PortScan |] + let d2 = [| Encryption ; Cypher |] + let d3 = [| Hardening ; Sanitation |] + + let rand = System.Random(System.Guid.NewGuid().GetHashCode()) + let getRandom (actions : 'a array) = actions.[rand.Next(0,2)] + + let weapons = [ getRandom h1 ; getRandom h2 ; getRandom h3 ] + let shields = [ getRandom d1 ; getRandom d2 ; getRandom d3 ] { DiscordId = membr - Hacks = [ Hack.Virus ; Hack.Worm ; Hack.Injection ] - Protections = [ Protection.Cypher ; Protection.Sanitation ; Protection.Firewall ] + Weapons = weapons + Shields = shields Attacks = [] - Bank = 0L - Defenses = [] } + Defenses = [] + Bank = 0f } let constructButtons (actionType : string) (playerInfo : string) (weapons : 'a list) = weapons @@ -75,19 +122,18 @@ let constructButtons (actionType : string) (playerInfo : string) (weapons : 'a l $"{actionType}-{hack}-{playerInfo}", $"{hack}")) -let notRegisteredYetMessage (ctx : InteractionContext) = +let createSimpleResponseAsync msg (ctx : InteractionContext) = async { let builder = DiscordInteractionResponseBuilder() - builder.Content <- $"You are not currently a hacker, first use the /redpill command to become one" - + builder.Content <- msg builder.AsEphemeral true |> ignore - do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask - } |> Async.StartAsTask :> Task +let notYetAHackerMsg = createSimpleResponseAsync "You are not currently a hacker, first use the /redpill command to become one" + let removeExpiredActions timespan (timestamp : 'a -> DateTime) actions = actions |> List.filter (fun act -> @@ -101,11 +147,13 @@ let constructEmbed message = builder.Description <- message let author = DiscordEmbedBuilder.EmbedAuthor() author.Name <- "Joebot Pro" - author.Url <- "https://ferano.io" - author.IconUrl <- "https://i.kym-cdn.com/entries/icons/original/000/028/861/cover3.jpg" + author.Url <- "https://twitter.com/degenzgame" + author.IconUrl <- "https://pbs.twimg.com/profile_images/1473192843359309825/cqjm0VQ4_400x400.jpg" builder.Author <- author builder.Build() +type EmptyGlobalCommandToAvoidFamousDuplicateSlashCommandsBug() = inherit ApplicationCommandModule () + type JoeBot() = inherit ApplicationCommandModule () @@ -141,6 +189,7 @@ type JoeBot() = if role.Name = "Hacker" then do! ctx.Member.RevokeRoleAsync(role) |> Async.AwaitTask + players <- players |> List.filter (fun p -> p.DiscordId <> ctx.User.Id) do! ctx.CreateResponseAsync("You are now lame", true) |> Async.AwaitTask } |> Async.StartAsTask @@ -148,47 +197,48 @@ type JoeBot() = [] member this.AttackCommand (ctx : InteractionContext, [] target : DiscordUser) = - // TODO: We need to check if the player has any active hacks going, if not they can cheat - players - |> List.tryFind (fun p -> p.DiscordId = ctx.Member.Id) - |> function - | Some player -> - let updatedAttacks = removeExpiredActions (TimeSpan.FromMinutes(15)) (fun (atk : Attack) -> atk.Timestamp) player.Attacks - players <- - players - |> List.map (fun p -> if p.DiscordId = player.DiscordId then { p with Attacks = updatedAttacks } else p) - if updatedAttacks.Length < 1 then - async { - let builder = DiscordInteractionResponseBuilder() - builder.AddEmbed (constructEmbed "Pick the hack you wish to use.") |> ignore - - let targetInfo = $"{target.Id}-{target.Username}" - constructButtons "Attack" targetInfo player.Hacks - |> Seq.cast - |> builder.AddComponents - |> ignore - - builder.AsEphemeral true |> ignore - - do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) - |> Async.AwaitTask - - } |> Async.StartAsTask - :> Task - else - async { - let builder = DiscordInteractionResponseBuilder() - let timeRemaining = TimeSpan.FromMinutes(15) - (DateTime.UtcNow - updatedAttacks.Head.Timestamp) - builder.Content <- $"You already hacked, please wait {timeRemaining.Minutes} minutes and {timeRemaining.Seconds} seconds to attempt another hack" - - builder.AsEphemeral true |> ignore - - do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) - |> Async.AwaitTask - - } |> Async.StartAsTask - :> Task - | None -> notRegisteredYetMessage ctx + // TODO: We need to check if the player has any active embed hacks going, if not they can cheat + let attacker = players |> List.tryFind (fun p -> p.DiscordId = ctx.Member.Id) + let defender = players |> List.tryFind (fun p -> p.DiscordId = target.Id) + match attacker , defender with + | Some attacker , Some defender -> + let updatedAttacks = removeExpiredActions (TimeSpan.FromMinutes(15)) (fun (atk : Attack) -> atk.Timestamp) attacker.Attacks + players <- + players + |> List.map (fun p -> if p.DiscordId = attacker.DiscordId then { p with Attacks = updatedAttacks } else p) + if updatedAttacks.Length < 2 then + async { + let builder = DiscordInteractionResponseBuilder() + builder.AddEmbed (constructEmbed "Pick the hack you wish to use.") |> ignore + + let defenderInfo = $"{defender.DiscordId}-{target.Username}" + constructButtons "Attack" defenderInfo attacker.Weapons + |> Seq.cast + |> builder.AddComponents + |> ignore + + builder.AsEphemeral true |> ignore + + do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask + + } |> Async.StartAsTask + :> Task + else + async { + let builder = DiscordInteractionResponseBuilder() + let timeRemaining = TimeSpan.FromMinutes(15) - (DateTime.UtcNow - updatedAttacks.Head.Timestamp) + builder.Content <- $"You already hacked, please wait {timeRemaining.Minutes} minutes and {timeRemaining.Seconds} seconds to attempt another hack" + + builder.AsEphemeral true |> ignore + + do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask + + } |> Async.StartAsTask + :> Task + | None , _ -> notYetAHackerMsg ctx + | _ , None -> createSimpleResponseAsync "Your target is not connected to the network, they must join first by using the /redpill command" ctx [] @@ -205,7 +255,7 @@ type JoeBot() = let builder = DiscordInteractionResponseBuilder() builder.AddEmbed (constructEmbed "Pick a defense to mount for a duration of time") |> ignore - constructButtons "Defense" (string player.DiscordId) player.Protections + constructButtons "Defend" (string player.DiscordId) player.Shields |> Seq.cast |> builder.AddComponents |> ignore @@ -217,71 +267,137 @@ type JoeBot() = } |> Async.StartAsTask :> Task - | None -> notRegisteredYetMessage ctx + | None -> notYetAHackerMsg ctx + + [] + member this.Status (ctx : InteractionContext) = + async { + return! + match players |> List.tryFind (fun p -> p.DiscordId = ctx.Member.Id) with + | Some player -> + async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- $"%A{player}" + do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask + } + | None -> notYetAHackerMsg ctx |> Async.AwaitTask + } |> Async.StartAsTask + :> Task + +let calculateDamage (hack : IClass) (protection : IClass) = + let hackClass = hack.GetClass() + let protectionClass = protection.GetClass() + match hackClass , protectionClass with + | h , p when h = p -> Weak + | _ -> Strong let handleAttack (event : ComponentInteractionCreateEventArgs) = + let updatePlayer amount attack p = { + p with Attacks = attack::p.Attacks + Bank = MathF.Max(p.Bank + amount, 0f) + } async { let split = event.Id.Split("-") - let ( resultHack , hackType ) = Enum.TryParse(typedefof, split.[1]) + let resultHack = Weapon.TryParse(split.[1]) let ( resultId , targetId ) = UInt64.TryParse split.[2] - match resultHack , resultId with - | true , true -> - let hackType = hackType :?> Hack - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- $"Sent {hackType} to {split.[3]}!" - do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) - |> Async.AwaitTask - - let attack = { HackType = hackType ; Timestamp = DateTime.UtcNow ; Target = { Id = targetId ; Name = split.[3] } } - players <- + return! + match resultHack , resultId with + | Some weapon , true -> + let hackType = weapon players - |> List.map (fun p -> if p.DiscordId = event.User.Id then { p with Attacks = attack::p.Attacks } else p) - - let builder = DiscordMessageBuilder() - builder.WithContent($"{event.User.Username} has sent a hack to <@{targetId}>") |> ignore - let battleChannel = (event.Guild.GetChannel(927449884204867664uL)) - do! battleChannel.SendMessageAsync(builder) - |> Async.AwaitTask - |> Async.Ignore - | _ -> - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- "Error parsing Button Id" - do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) - |> Async.AwaitTask + |> List.find (fun p -> p.DiscordId = targetId) + |> fun p -> p.Defenses + |> List.map (fun dfn -> dfn.DefenseType) + |> List.map (calculateDamage hackType) + |> List.contains Weak + |> function + | false -> + async { + let prize = 0.1726f + let attack = { HackType = hackType ; Timestamp = DateTime.UtcNow ; Target = { Id = targetId ; Name = split.[3] } } + players <- + players + |> List.map (fun p -> if p.DiscordId = event.User.Id then updatePlayer prize attack p else p) + + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- $"Successfully hacked {split.[3]} using {hackType}! You just won {prize} genz!" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) + |> Async.AwaitTask + + let builder = DiscordMessageBuilder() + builder.WithContent($"{event.User.Username} successfully hacked <@{targetId}>!") |> ignore + let battleChannel = (event.Guild.GetChannel(927449884204867664uL)) + do! battleChannel.SendMessageAsync(builder) + |> Async.AwaitTask + |> Async.Ignore + } + | true -> + async { + let builder = DiscordInteractionResponseBuilder() + let loss = -0.0623f + builder.IsEphemeral <- true + builder.Content <- $"Hack failed! {split.[3]} was able to mount a successful defense! You lost {loss} genz!" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) + |> Async.AwaitTask + + let attack = { HackType = hackType ; Timestamp = DateTime.UtcNow ; Target = { Id = targetId ; Name = split.[3] } } + players <- + players + |> List.map (fun p -> if p.DiscordId = event.User.Id then updatePlayer loss attack p else p) + + let builder = DiscordMessageBuilder() + builder.WithContent($"{event.User.Username} failed to hack <@{targetId}>!") |> ignore + let battleChannel = (event.Guild.GetChannel(927449884204867664uL)) + do! battleChannel.SendMessageAsync(builder) + |> Async.AwaitTask + |> Async.Ignore + } + | _ -> + async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- "Error parsing Button Id" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask + } } let handleDefense (event : ComponentInteractionCreateEventArgs) = async { let split = event.Id.Split("-") - let ( resultHack , protectionType ) = Enum.TryParse(typedefof, split.[1]) - match resultHack with - | true -> - let protectionType = protectionType :?> Protection - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- $"Mounted a {protectionType} defense for 1 hour" - do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) - |> Async.AwaitTask - - let defense = { DefenseType = protectionType ; Timestamp = DateTime.UtcNow } - players <- - players - |> List.map (fun p -> { p with Defenses = defense::p.Defenses }) - - let builder = DiscordMessageBuilder() - builder.WithContent($"{event.User.Username} has protected their system!") |> ignore - let battleChannel = (event.Guild.GetChannel(927449884204867664uL)) - do! battleChannel.SendMessageAsync(builder) - |> Async.AwaitTask - |> Async.Ignore - | _ -> - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- "Error parsing Button Id" - do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) - |> Async.AwaitTask + return! + match Shield.TryParse(split.[1]) with + | Some shield -> + async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- $"Mounted a {shield} defense for 1 hour" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) + |> Async.AwaitTask + + let defense = { DefenseType = shield ; Timestamp = DateTime.UtcNow } + players <- + players + |> List.map (fun p -> { p with Defenses = defense::p.Defenses }) + + let builder = DiscordMessageBuilder() + builder.WithContent($"{event.User.Username} has protected their system!") |> ignore + let battleChannel = (event.Guild.GetChannel(927449884204867664uL)) + do! battleChannel.SendMessageAsync(builder) + |> Async.AwaitTask + |> Async.Ignore + } + | _ -> + async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- "Error parsing Button Id" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask + } } let handleButtonEvent (client : DiscordClient) (event : ComponentInteractionCreateEventArgs) = @@ -289,7 +405,14 @@ let handleButtonEvent (client : DiscordClient) (event : ComponentInteractionCrea return! match event.Id with | id when id.StartsWith("Attack") -> handleAttack event | id when id.StartsWith("Defend") -> handleDefense event - | _ -> async { return () } + | _ -> + async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- $"Incorrect Action identifier {event.Id}" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask + } } |> Async.StartAsTask :> Task