discord-bot-game/Bot/Games/SlotMachine.fs

443 lines
20 KiB
Forth
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module Degenz.SlotMachine
open System
open System.Threading.Tasks
open DSharpPlus
open DSharpPlus.Entities
open DSharpPlus.EventArgs
open Degenz
open Degenz.Messaging
open Degenz.Types
open Npgsql.FSharp
type SlotSymbol = {
index : int
emojiName : string
reel1Count : int
reel2Count : int
reel3Count : int
}
type Prize =
| Money of int<GBT>
| Jackpot
type Slot =
| Symbol of SlotSymbol
| Any
let BigBrother = { index = 0 ; reel1Count = 2 ; reel2Count = 2 ; reel3Count = 2 ; emojiName = "bigbrother" }
let Eye = { index = 1 ; reel1Count = 2 ; reel2Count = 3 ; reel3Count = 2 ; emojiName = "aneye" }
let Obey = { index = 2 ; reel1Count = 3 ; reel2Count = 3 ; reel3Count = 3 ; emojiName = "obey" }
let AnonMask = { index = 3 ; reel1Count = 2 ; reel2Count = 2 ; reel3Count = 4 ; emojiName = "anonmask" }
let Rat = { index = 4 ; reel1Count = 4 ; reel2Count = 4 ; reel3Count = 4 ; emojiName = "rat" }
let Ramen = { index = 5 ; reel1Count = 4 ; reel2Count = 5 ; reel3Count = 5 ; emojiName = "ramen" }
let Sushi = { index = 6 ; reel1Count = 5 ; reel2Count = 5 ; reel3Count = 4 ; emojiName = "sushi" }
let Pizza = { index = 7 ; reel1Count = 3 ; reel2Count = 3 ; reel3Count = 4 ; emojiName = "pizza" }
//let Alcohol = { index = 8 ; reel1Count = 1 ; reel2Count = 1 ; reel3Count = 1 ; emojiName = "alcohol" }
//let Circuit = { index = 9 ; reel1Count = 0 ; reel2Count = 0 ; reel3Count = 2 ; emojiName = "circuitboard" }
//let OldTv = { index = 10 ; reel1Count = 1 ; reel2Count = 2 ; reel3Count = 2 ; emojiName = "oldtv" }
//let Pills = { index = 11 ; reel1Count = 2 ; reel2Count = 1 ; reel3Count = 2 ; emojiName = "pills" }
//let symbols = [ BigBrother ; Eye ; Obey ; AnonMask ; Sushi ; Ramen ; Pizza ; Alcohol ; Circuit ; OldTv ; Pills ; Rat ]
let symbols = [ BigBrother ; Eye ; Obey ; AnonMask ; Sushi ; Ramen ; Pizza ; Rat ]
type PrizeEntry = Slot * Slot * Slot * Prize
let prizeTable =
[| Symbol BigBrother , Symbol BigBrother , Symbol BigBrother , Jackpot
Symbol Eye , Symbol Eye , Symbol Eye , Money 500<GBT>
Symbol Eye , Symbol Eye , Symbol AnonMask , Money 500<GBT>
Symbol AnonMask , Symbol AnonMask , Symbol AnonMask , Money 250<GBT>
Symbol Obey , Symbol Obey , Symbol Obey , Money 250<GBT>
Symbol Obey , Symbol Obey , Symbol AnonMask , Money 200<GBT>
Symbol Sushi , Symbol Sushi , Symbol Sushi , Money 100<GBT>
Symbol Ramen , Symbol Ramen , Symbol Ramen , Money 100<GBT>
Symbol Pizza , Symbol Pizza , Symbol Pizza , Money 100<GBT>
// Symbol Alcohol , Symbol Alcohol , Symbol Alcohol , Money 100<GBT>
// Symbol OldTv , Symbol OldTv , Symbol OldTv , Money 100<GBT>
// Symbol Pills , Symbol Pills , Symbol Pills , Money 100<GBT>
Symbol Rat , Symbol Rat , Symbol Rat , Money 100<GBT>
Symbol Sushi , Symbol Sushi , Any , Money 50<GBT>
Any , Any , Symbol Pizza , Money 20<GBT> |]
let calculateOdds (prizeTable : PrizeEntry array) prizeIndex =
let totalPerReel (reel : SlotSymbol -> int) = List.sumBy reel symbols
let r1 s = s.reel1Count
let r2 s = s.reel2Count
let r3 s = s.reel3Count
let totalForReel1 = function Symbol sym -> r1 sym | Any -> totalPerReel (fun s -> s.reel1Count)
let totalForReel2 = function Symbol sym -> r2 sym | Any -> totalPerReel (fun s -> s.reel2Count)
let totalForReel3 = function Symbol sym -> r3 sym | Any -> totalPerReel (fun s -> s.reel3Count)
let s1 , s2 , s3 , _ = prizeTable.[prizeIndex]
totalForReel1 s1 * totalForReel2 s2 * totalForReel3 s3
let getTotalCombinations symbols =
(List.sumBy (fun s -> s.reel1Count) symbols)
* (List.sumBy (fun s -> s.reel2Count) symbols)
* (List.sumBy (fun s -> s.reel3Count) symbols)
let getOddsForPrize symbols prizeTable prizeIndex =
let odds = calculateOdds prizeTable prizeIndex
let total = getTotalCombinations symbols
$"{odds} in {total }"
let getTotalWaysOfWinning (prizeTable : PrizeEntry array) =
[0..prizeTable.Length - 1]
|> List.sumBy (calculateOdds prizeTable)
let printOddsTable symbols (prizeTable : PrizeEntry array) =
([0..prizeTable.Length - 1]
|> List.fold (fun acc elem ->
let name = function Symbol s -> s.emojiName | Any -> "Any"
let s1 , s2, s3, _ = prizeTable.[elem]
acc + $"{name s1} | {name s2} | {name s3} --- {getOddsForPrize symbols prizeTable elem}\n") "")
+ "-----------------------------\n"
+ $"Odds of winning something: {getTotalWaysOfWinning prizeTable} out of {getTotalCombinations symbols}"
//printOddsTable symbols prizeTable
let getReel fn = List.fold (fun acc elem -> (List.replicate (fn elem) elem) @ acc) [] symbols |> List.toArray
let reel1 = getReel (fun s -> s.reel1Count)
let reel2 = getReel (fun s -> s.reel2Count)
let reel3 = getReel (fun s -> s.reel3Count)
let slotEmojiNames =
[| "sushi"
"bigbrother"
"pizza"
"ramen"
"circuitboard"
"obey"
"pills"
"oldtv"
"rat"
"aneye"
"alcohol"
"anonmask" |]
let PlayPricex1 = 10<GBT>
let PlayPricex2 = 20<GBT>
let PlayPricex3 = 30<GBT>
let BaseJackpotAmount = 0<GBT>
let sleepTime = 1500
let mutable guildEmojis : Map<string, DiscordEmoji> option = None
let mutable anyEmoji : DiscordEmoji option = None
let getJackpotAmount () =
GuildEnvironment.connectionString
|> Sql.connect
|> Sql.query """
SELECT stock FROM store_item
WHERE store_item.item_id = (SELECT id FROM item WHERE symbol = 'JACKPOT')
"""
|> Sql.executeRowAsync (fun read -> (read.int "stock") * 1<GBT>)
|> Async.AwaitTask
let incrementJackpot amount =
GuildEnvironment.connectionString
|> Sql.connect
|> Sql.parameters [ ( "amount" , Sql.int (int amount) ) ]
|> Sql.query """
UPDATE store_item SET stock = stock + @amount
WHERE store_item.item_id = (SELECT id FROM item WHERE symbol = 'JACKPOT')
"""
|> Sql.executeNonQueryAsync
|> Async.AwaitTask
let resetJackpot amount =
GuildEnvironment.connectionString
|> Sql.connect
|> Sql.parameters [ ( "amount" , Sql.int (int amount) ) ]
|> Sql.query """
UPDATE store_item SET stock = @amount
WHERE store_item.item_id = (SELECT id FROM item WHERE symbol = 'JACKPOT')
"""
|> Sql.executeNonQueryAsync
|> Async.AwaitTask
let getLastPlayedSlotFromPlayer (did : uint64) = async {
let! events =
GuildEnvironment.connectionString
|> Sql.connect
|> Sql.parameters [ "did", Sql.string (string did) ]
|> Sql.query """
SELECT player_event.updated_at FROM player_event
JOIN "user" u on u.id = player_event.user_id
WHERE u.discord_id = @did AND event_type = 'PlayingSlot'
"""
|> Sql.executeAsync (fun read -> read.dateTime "updated_at" )
|> Async.AwaitTask
match events with
| [] -> return None
| es -> return Some (List.head es)
}
let updateSlotPlayedFromPlayer (did : uint64) =
GuildEnvironment.connectionString
|> Sql.connect
|> Sql.parameters [ "did", Sql.string (string did) ]
|> Sql.query """
WITH usr AS (SELECT id FROM "user" WHERE discord_id = @did)
UPDATE player_event SET updated_at = now() at time zone 'utc'
FROM usr WHERE usr.id = user_id AND player_event.event_type = 'PlayingSlot';
"""
|> Sql.executeNonQueryAsync
|> Async.AwaitTask
|> Async.Ignore
let handlePrizeTable (ctx : IDiscordContext) =
task {
do! Messaging.defer ctx
let embed = DiscordEmbedBuilder()
match guildEmojis , anyEmoji with
| Some emojis , Some any ->
let folder (i,acc) elem =
let s1,s2,s3,prize = elem
let prizeTxt = match prize with Money m -> $"**{m}** $GBT" | Jackpot -> $"**JACKPOT**"
let line =
let getEmoji = function Symbol s -> Formatter.Emoji(emojis.[s.emojiName]) | Any -> Formatter.Emoji(any)
$"{getEmoji s1}{getEmoji s2}{getEmoji s3}"
let text =
if prizeTxt.Contains("100") then
acc
else
$"{acc}\n{line} {prizeTxt}"
(i + 1) , text
let! jackpot = getJackpotAmount ()
let _ , rows = Array.fold folder (0, "") prizeTable
embed.Color <- DiscordColor.Green
embed.Title <- $"CURRENT JACKPOT: `{jackpot} 💰 $GBT`"
embed.Description <- embed.Description + $"\n\n**PRIZE TABLE **\n{rows}\n\n**ANY SAME 3** | **100** $GBT"
do! ctx.FollowUp(DiscordFollowupMessageBuilder().AsEphemeral().AddEmbed(embed))
do! Analytics.prizeTableViewed (ctx.GetDiscordMember())
| _ , _ -> return ()
} :> Task
let spinEmojis (builder : DiscordFollowupMessageBuilder) (results : SlotSymbol array) (itx : DiscordInteraction) =
async {
builder.Content <- "**Spinning!**"
let! followUp = itx.CreateFollowupMessageAsync(builder) |> Async.AwaitTask
match guildEmojis with
| Some emojis ->
let e1 = Formatter.Emoji(emojis.[results.[0].emojiName])
let e2 = Formatter.Emoji(emojis.[results.[1].emojiName])
let e3 = Formatter.Emoji(emojis.[results.[2].emojiName])
do! Async.Sleep sleepTime
let content = $"{e1}"
let! _ = itx.EditFollowupMessageAsync(followUp.Id, DiscordWebhookBuilder().WithContent(content)) |> Async.AwaitTask
do! Async.Sleep sleepTime
let content = $"{e1}{e2}"
let! _ = itx.EditFollowupMessageAsync(followUp.Id, DiscordWebhookBuilder().WithContent(content)) |> Async.AwaitTask
do! Async.Sleep sleepTime
let content = $"{e1}{e2}{e3}"
let! _ = itx.EditFollowupMessageAsync(followUp.Id, DiscordWebhookBuilder().WithContent(content)) |> Async.AwaitTask
return followUp , content
| None ->
let! _ = itx.EditFollowupMessageAsync(followUp.Id, DiscordWebhookBuilder().WithContent("An error occurred, please contact an Admin")) |> Async.AwaitTask
return followUp , ""
}
let spin multiplier (ctx : IDiscordContext) =
let playAmount =
match multiplier with
| 1 -> PlayPricex1
| 2 -> PlayPricex2
| _ -> PlayPricex3
let execute player = async {
do! DbService.updatePlayerCurrency -playAmount player |> Async.Ignore
let random = System.Random(System.Guid.NewGuid().GetHashCode())
let results = [| reel1.[random.Next(0, reel1.Length)] ; reel2.[random.Next(0, reel2.Length)] ; reel3.[random.Next(0, reel3.Length)] |]
// let results = [| BigBrother ; BigBrother ; BigBrother |]
let getPrize (results : SlotSymbol array) =
prizeTable
|> Array.tryPick (fun (s1,s2,s3,prize) ->
let getIndex = function Symbol s -> s.index | Any -> -1
let indices = [ s1 ; s2 ; s3 ] |> List.map getIndex
let compare i1 i2 = i1 = i2 || i1 = -1
let s1Result = compare indices.[0] results.[0].index
let s2Result = compare indices.[1] results.[1].index
let s3Result = compare indices.[2] results.[2].index
if s1Result && s2Result && s3Result
then Some prize
else None)
let prize = getPrize results
let builder = DiscordFollowupMessageBuilder()
builder.IsEphemeral <- true
let itx = ctx.GetInteraction()
let! followUpMessage , slotsContent = spinEmojis builder results itx
do! Async.Sleep 2000
let embed = DiscordEmbedBuilder()
embed.Author <- DiscordEmbedBuilder.EmbedAuthor()
embed.Author.Name <- player.Name
embed.Author.IconUrl <- ctx.GetDiscordMember().AvatarUrl
embed.Title <- "Slots Results"
let addGBTField (embed : DiscordEmbedBuilder) prize =
if prize > 0<GBT>
then embed.AddField("New $GBT Balance", $"`💰` {player.Bank} `💰` {player.Bank - playAmount + prize} `(+{prize - playAmount} $GBT)`") |> ignore
else embed.AddField("New $GBT Balance", $"`💰` {player.Bank} `💰` {player.Bank + prize} `({prize} $GBT)`") |> ignore
let! result , prizeAmount = async {
match prize with
| Some (Money amount) ->
let prizeWithMultiplier = amount * multiplier
do! DbService.updatePlayerCurrency prizeWithMultiplier player |> Async.Ignore
embed.Color <- DiscordColor.Green
embed.Description <- $"You WIN `💰` **{prizeWithMultiplier}** GBT 🎉"
embed.AddField("Bet", $"{playAmount}", true) |> ignore
// embed.AddField("Jackpot Rake", $"{multiplier}", true) |> ignore
embed.AddField("Prize", $"{prizeWithMultiplier}", true) |> ignore
addGBTField embed prizeWithMultiplier
// do! incrementJackpot multiplier |> Async.Ignore
return "WON" , prizeWithMultiplier
| Some (Jackpot) ->
let! jackpot = getJackpotAmount ()
embed.Color <- DiscordColor.Green
embed.Description <- $"🎉🎉 YOU HIT THE JACKPOT 🎉🎉"
embed.AddField("Bet", $"{playAmount}", true) |> ignore
embed.AddField("Prize", $"{jackpot}", true) |> ignore
addGBTField embed jackpot
do! DbService.updatePlayerCurrency jackpot player |> Async.Ignore
do! resetJackpot BaseJackpotAmount |> Async.Ignore
return "JACKPOT" , jackpot
| None ->
do! incrementJackpot 1 |> Async.Ignore
embed.Description <- $"You LOST `💰` **{playAmount}** $GBT 😭"
embed.Color <- DiscordColor.Red
embed.AddField("Bet", $"{playAmount}", true) |> ignore
embed.AddField("Jackpot Rake", $"{multiplier}", true) |> ignore
addGBTField embed -playAmount
return "LOST" , 0<GBT>
}
let dwb = DiscordWebhookBuilder()
let button = DiscordButtonComponent(ButtonStyle.Success, $"spin-{multiplier}x", $"🎰 Spin Again") :> DiscordComponent
dwb.AddComponents([| button |]).AddEmbed(embed).WithContent(slotsContent) |> ignore
do! itx.EditFollowupMessageAsync(followUpMessage.Id, dwb) |> Async.AwaitTask |> Async.Ignore
let hopMsg = DiscordMessageBuilder()
hopMsg.Content <-
let s1 = $"{slotsContent} | {player.Name}"
match result with
| "WON" -> s1 + $" **WON** {prizeAmount} $GBT playing slots!"
| "LOST" -> s1 + $" **LOST** playing slots"
| "JACKPOT" -> s1 + $" 🎉🎉🎉 **JUST HIT THE JACKPOT** and **WON** {prizeAmount}!!! 🎉🎉🎉"
| _ -> ""
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(hopMsg) |> Async.AwaitTask |> Async.Ignore
do! Analytics.slotPlayed (ctx.GetDiscordMember()) playAmount result prizeAmount
}
PlayerInteractions.executePlayerAction ctx (fun player -> async {
if player.Bank >= playAmount then
match! getLastPlayedSlotFromPlayer player.DiscordId with
| Some timestamp ->
if DateTime.UtcNow - timestamp > TimeSpan.FromSeconds(8) then
do! updateSlotPlayedFromPlayer player.DiscordId
do! execute player
else
do! Messaging.sendFollowUpMessage ctx "Wait till you finish the current spin!"
| None ->
let event = { Type = PlayingSlot ; Cooldown = 1<mins> ; Timestamp = DateTime.UtcNow }
do! DbService.addPlayerEvent player.DiscordId event |> Async.Ignore
do! execute player
else
do! Messaging.sendFollowUpMessage ctx "You do not have sufficient funds to play"
})
let handleButton (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) =
let ctx = DiscordEventContext event
match event.Id with
| "spin-1x" -> spin 1 ctx
| "spin-2x" -> spin 2 ctx
| "spin-3x" -> spin 3 ctx
| "prizes" -> handlePrizeTable ctx
| _ ->
printfn "Wrong Spin ID"
Task.CompletedTask
|> Async.AwaitTask
|> Async.Start
Task.CompletedTask
let handleGuildDownloadCompleted (_ : DiscordClient) (event : GuildDownloadCompletedEventArgs) =
task {
let ( result , guild ) = event.Guilds.TryGetValue(GuildEnvironment.guildId)
guildEmojis <-
guild.Emojis
|> Seq.map (fun kvp -> kvp.Value) |> Seq.toArray
|> Seq.filter (fun de -> Array.contains de.Name slotEmojiNames)
|> Seq.distinctBy (fun de -> de.Name)
|> Seq.map (fun de -> ( de.Name , de ))
|> Map.ofSeq
|> Some
anyEmoji <- guild.Emojis |> Seq.tryPick (fun kvp -> if kvp.Value.Name = "any" then Some kvp.Value else None)
return ()
} :> Task
let handleMessageCreated (_ : DiscordClient) (event : MessageCreateEventArgs) =
task {
if event.Channel.Id = GuildEnvironment.channelSlots && event.Author.Id <> GuildEnvironment.botClientSlots.Value.CurrentUser.Id then
do! Async.Sleep 1000
do! event.Message.DeleteAsync()
} :> Task
let sendEmbed (jackpotNumChannel : DiscordChannel) slotsChannel (message : DiscordMessage option) = async {
let builder = DiscordMessageBuilder()
let embed = DiscordEmbedBuilder()
embed.Title <- "Degenz Slot Machine"
embed.ImageUrl <- "https://s7.gifyu.com/images/ezgif.com-gif-maker-268ecb6e4d28bd55a0.gif"
embed.Description <- "Try Your Hand at a Game of SLOTS!\nEvery Spin Adds to The JACKPOT!\nGood luck Degenz ✊"
let! jackpot = getJackpotAmount ()
embed.Title <- $"CURRENT JACKPOT: `{jackpot} 💰 $GBT`"
// Update Jackpot channel
// try
// do! jackpotNumChannel.ModifyAsync(fun channelEditModel -> channelEditModel.Name <- $"Jackpot: {jackpot} 💰 $GBT") |> Async.AwaitTask
// with ex -> printfn $"Error changing name: {ex.Message}"
builder.AddEmbed(embed) |> ignore
let button1 = DiscordButtonComponent(ButtonStyle.Success, $"spin-1x", $"🎰 Bet {PlayPricex1} $GBT") :> DiscordComponent
let button2 = DiscordButtonComponent(ButtonStyle.Success, $"spin-2x", $"🎰 Bet {PlayPricex2} $GBT") :> DiscordComponent
let button3 = DiscordButtonComponent(ButtonStyle.Success, $"spin-3x", $"🎰 Bet {PlayPricex3} $GBT") :> DiscordComponent
let button4 = DiscordButtonComponent(ButtonStyle.Primary, $"prizes", $"Prize Table") :> DiscordComponent
builder.AddComponents([| button1 ; button2 ; button3 ; button4 |]) |> ignore
match GuildEnvironment.botClientSlots, message with
| _ , Some m ->
let! _ = m.ModifyAsync(builder) |> Async.AwaitTask
return Some m
| Some bot , None ->
let! m = bot.SendMessageAsync(slotsChannel, builder) |> Async.AwaitTask
do! m.PinAsync() |> Async.AwaitTask
return Some m
| _ -> return None
}
let sendEmbedWithLoop (jackpotNumChannel : DiscordChannel) (slotsChannel : DiscordChannel) =
async {
let! pins = slotsChannel.GetPinnedMessagesAsync() |> Async.AwaitTask
let! message =
match pins |> Seq.toList with
| [] -> sendEmbed jackpotNumChannel slotsChannel None
| msg::_ -> async.Return (Some msg)
while true do
do! Async.Sleep 305000
do! sendEmbed jackpotNumChannel slotsChannel message |> Async.Ignore
} |> Async.Start
let sendInitialEmbedFromLaunch (client : DiscordClient) =
// let jackpotNumChannel = client.Guilds.[GuildEnvironment.guildId].GetChannel(GuildEnvironment.channelJackpotNum)
let slotsChannel = client.Guilds.[GuildEnvironment.guildId].GetChannel(GuildEnvironment.channelSlots)
sendEmbedWithLoop slotsChannel slotsChannel
let sendInitialEmbedFromSlashCommand (ctx : IDiscordContext) =
// let jackpotNumChannel = ctx.GetGuild().GetChannel(GuildEnvironment.channelJackpotNum)
let slotsChannel = ctx.GetGuild().GetChannel(GuildEnvironment.channelSlots)
sendEmbedWithLoop slotsChannel slotsChannel