Compare commits
No commits in common. "master" and "currency-api" have entirely different histories.
master
...
currency-a
179
Airdrop.fsx
179
Airdrop.fsx
@ -1,179 +0,0 @@
|
||||
#load "/home/joe/Development/DegenzGame/.paket/load/net6.0/main.group.fsx";;
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
open Npgsql.FSharp
|
||||
open dotenv.net
|
||||
open Solnet.Rpc
|
||||
open Solnet.Rpc.Models
|
||||
open Solnet.Rpc.Builders
|
||||
open Solnet.Wallet
|
||||
open Solnet.Programs
|
||||
open Solnet.KeyStore
|
||||
|
||||
let devEnv = DotEnv.Read(DotEnvOptions(envFilePaths = [ "./.dev.env" ]))
|
||||
|
||||
let ( _ , devConnStr )= devEnv.TryGetValue("DATABASE_URL")
|
||||
|
||||
let keystore = SolanaKeyStoreService()
|
||||
let file = File.ReadAllText("/home/joe/.config/solana/devnet.json")
|
||||
let authority = keystore.RestoreKeystore(file)
|
||||
|
||||
//let rpcClient = ClientFactory.GetClient("https://still-empty-field.solana-mainnet.quiknode.pro/5b1b6b5c913ec79a20bef19d5ba5f63023e470d6/")
|
||||
let rpcClient = ClientFactory.GetClient(Cluster.DevNet)
|
||||
|
||||
let mintAccount = PublicKey("2iS6gcoB5VhiLC4eNB7NdcaLgEHjLrXHYpz7T2JMGBDw")
|
||||
//let associatedTokenAccountOwner = PublicKey("GutKESfJw8PDMbFVqByxTr4f5TUSHUVmkf5gtsWWWqrU")
|
||||
|
||||
type AirdropStatus =
|
||||
| Hold = 0
|
||||
| Ready = 1
|
||||
| Pending = 2
|
||||
| Error = 3
|
||||
| PendingError = 4
|
||||
| Completed = 5
|
||||
| Verified = 6
|
||||
|
||||
let updateError wallet msg =
|
||||
devConnStr
|
||||
|> Sql.connect
|
||||
|> Sql.parameters [ "wallet" , Sql.string wallet ; "msg" , Sql.string msg ]
|
||||
|> Sql.query """
|
||||
UPDATE crypto SET error_msg = @msg, status = 'Error' WHERE crypto.wallet_address = @wallet;
|
||||
"""
|
||||
|> Sql.executeNonQueryAsync
|
||||
|
||||
let updatePendingError wallet msg txId =
|
||||
devConnStr
|
||||
|> Sql.connect
|
||||
|> Sql.parameters [ "wallet" , Sql.string wallet ; "msg" , Sql.string msg ; "txId" , Sql.string txId ]
|
||||
|> Sql.query """
|
||||
UPDATE crypto SET error_msg = @msg, pending_tx = @txId, status = 'PendingError' WHERE crypto.wallet_address = @wallet;
|
||||
"""
|
||||
|> Sql.executeNonQueryAsync
|
||||
|
||||
let updatePending wallet txId =
|
||||
devConnStr
|
||||
|> Sql.connect
|
||||
|> Sql.parameters [ "wallet" , Sql.string wallet ; "txId" , Sql.string txId ]
|
||||
|> Sql.query """
|
||||
UPDATE crypto SET pending_tx = @txId, status = 'Pending' WHERE crypto.wallet_address = @wallet;
|
||||
"""
|
||||
|> Sql.executeNonQueryAsync
|
||||
|
||||
let updateCompleted wallet txId =
|
||||
devConnStr
|
||||
|> Sql.connect
|
||||
|> Sql.parameters [ "wallet" , Sql.string wallet ; "txId" , Sql.string txId ]
|
||||
|> Sql.query """
|
||||
UPDATE crypto SET successful_tx = pending_tx, status = 'Completed' WHERE crypto.wallet_address = @wallet;
|
||||
"""
|
||||
|> Sql.executeNonQueryAsync
|
||||
|
||||
let updateVerified wallet txId amount =
|
||||
devConnStr
|
||||
|> Sql.connect
|
||||
|> Sql.parameters [ "wallet" , Sql.string wallet ; "txId" , Sql.string txId ; "amount" , Sql.int amount ]
|
||||
|> Sql.query """
|
||||
UPDATE crypto SET pending_tx = @txId, status = 'Verified', total_dropped = @amount WHERE crypto.wallet_address = @wallet;
|
||||
"""
|
||||
|> Sql.executeNonQueryAsync
|
||||
|
||||
let getTokenCountToDrop wallet =
|
||||
task {
|
||||
let! wl =
|
||||
devConnStr
|
||||
|> Sql.connect
|
||||
|> Sql.parameters [ "wallet" , Sql.string wallet ]
|
||||
|> Sql.query """
|
||||
SELECT has_wl, has_og FROM crypto WHERE wallet_address = @wallet;
|
||||
"""
|
||||
|> Sql.executeRowAsync (fun reader -> {| HasWhitelist = reader.bool "has_wl" ; HasOg = reader.bool "has_og" |})
|
||||
return (if wl.HasWhitelist then 1uL else 0uL) + (if wl.HasOg then 2uL else 0uL)
|
||||
}
|
||||
|
||||
// TODO: Change was_successful to status and check if attempted, pending, errored, or completed
|
||||
let executeDrop (wallet : string) =
|
||||
let associatedTokenAccountOwner = PublicKey(wallet)
|
||||
let associatedTokenAccount = AssociatedTokenAccountProgram.DeriveAssociatedTokenAccount(associatedTokenAccountOwner, mintAccount)
|
||||
let log msg = printfn $"{wallet} || {msg}"
|
||||
|
||||
log $"ATA: {associatedTokenAccount.Key}"
|
||||
let buildTransaction (block : LatestBlockHash) amount (targetWallet : string) =
|
||||
TransactionBuilder()
|
||||
.SetRecentBlockHash(block.Blockhash)
|
||||
.SetFeePayer(authority.Account)
|
||||
.AddInstruction(AssociatedTokenAccountProgram.CreateAssociatedTokenAccount(
|
||||
authority.Account,
|
||||
PublicKey(targetWallet),
|
||||
mintAccount))
|
||||
.AddInstruction(TokenProgram.Transfer(
|
||||
PublicKey("CRa7GCMUaB4np32XoTD2sEyCXFVfYKKF4JRPkQTwV3EY"),
|
||||
associatedTokenAccount,
|
||||
amount,
|
||||
authority.Account))
|
||||
.Build(authority.Account);
|
||||
task {
|
||||
// let streamingClient = ClientFactory.GetStreamingClient("wss://still-empty-field.solana-mainnet.quiknode.pro/5b1b6b5c913ec79a20bef19d5ba5f63023e470d6/")
|
||||
let streamingClient = ClientFactory.GetStreamingClient(Cluster.DevNet)
|
||||
do! streamingClient.ConnectAsync()
|
||||
let blockHash = rpcClient.GetLatestBlockHash()
|
||||
let! amount = getTokenCountToDrop wallet
|
||||
log $"Dropping {amount} tokens"
|
||||
let tx = buildTransaction blockHash.Result.Value amount wallet
|
||||
let! tx = rpcClient.SendTransactionAsync(tx)
|
||||
if tx.ErrorData <> null then
|
||||
let! _ = updateError wallet (tx.ErrorData.Logs |> String.concat "\n")
|
||||
()
|
||||
log $"Transaction error: {wallet}"
|
||||
return ()
|
||||
elif String.IsNullOrWhiteSpace(tx.Result) then
|
||||
let msg = $"Transaction did not have an ID but the ErrorData was null: {tx.Reason}"
|
||||
let! _ = updateError wallet msg
|
||||
log $"Odd Transaction Error"
|
||||
return ()
|
||||
else
|
||||
log $"Successful, now waiting for RPC"
|
||||
let! _ = updatePending wallet tx.Result
|
||||
let! _ = streamingClient.SubscribeSignatureAsync(tx.Result, fun _ result ->
|
||||
if result.Value.Error = null then
|
||||
log "RPC Finished"
|
||||
task {
|
||||
let! _ = updateCompleted wallet tx.Result
|
||||
log "Getting Transaction and Token Balance"
|
||||
let! txInfo = rpcClient.GetTransactionAsync(tx.Result)
|
||||
let! tokenBalance = rpcClient.GetTokenAccountBalanceAsync(associatedTokenAccount)
|
||||
|
||||
|
||||
log $"Transaction Successful? {txInfo.WasSuccessful} - Token Balance {tokenBalance.Result.Value.AmountUlong}"
|
||||
if txInfo.WasSuccessful = true && tokenBalance.Result.Value.AmountUlong = amount then
|
||||
let! _ = updateVerified wallet tx.Result (int amount)
|
||||
()
|
||||
return ()
|
||||
} |> Async.AwaitTask |> Async.Start
|
||||
else
|
||||
let msg = $"Got an error, let's check it out {result.Value.Error}"
|
||||
updatePendingError wallet msg tx.Result |> Async.AwaitTask |> Async.Ignore |> Async.Start)
|
||||
return ()
|
||||
}
|
||||
|
||||
let targetWallets =
|
||||
devConnStr
|
||||
|> Sql.connect
|
||||
|> Sql.query """
|
||||
SELECT wallet_address FROM crypto WHERE status = 'Ready';
|
||||
"""
|
||||
|> Sql.execute (fun reader -> reader.string "wallet_address")
|
||||
|
||||
printfn $"Got target wallets: {targetWallets.Length}"
|
||||
|
||||
// "GK7rkZYrdAEpTm9n9TkHWK1T5nDXeRfVUVfcHQwSDyuJ"
|
||||
let asyncs =
|
||||
targetWallets
|
||||
|> List.map executeDrop
|
||||
|> List.map Async.AwaitTask
|
||||
|
||||
Async.Parallel ( asyncs , 16 ) |> Async.StartChild
|
||||
|
||||
|
||||
Console.ReadLine() |> ignore
|
Binary file not shown.
Before Width: | Height: | Size: 79 KiB |
Binary file not shown.
Before Width: | Height: | Size: 41 KiB |
Binary file not shown.
Before Width: | Height: | Size: 121 KiB |
Binary file not shown.
Before Width: | Height: | Size: 191 KiB |
35
Bot/Admin.fs
35
Bot/Admin.fs
@ -125,35 +125,6 @@ let getUserInvites userIds =
|
||||
|> Sql.executeAsync (fun read -> {| Username = read.string "display_name" ; DiscordId = read.string "inviter" |> uint64 ; TotalInvites = read.int "total_invites" |})
|
||||
|> Async.AwaitTask
|
||||
|
||||
type MerrisItem =
|
||||
| NuuPing = 0
|
||||
| Noodles = 1
|
||||
| Pizza = 2
|
||||
|
||||
let updateMerrisItem itemName amount =
|
||||
let itemId =
|
||||
match itemName with
|
||||
| MerrisItem.Noodles -> "NOODLES"
|
||||
| MerrisItem.Pizza -> "PIZZA"
|
||||
| MerrisItem.NuuPing | _ -> "GRILLED_RAT"
|
||||
|
||||
GuildEnvironment.connectionString
|
||||
|> Sql.connect
|
||||
|> Sql.parameters [ "itemId" , Sql.string itemId ; "amount" , Sql.int amount ]
|
||||
|> Sql.query "UPDATE mart_item SET stock = @amount WHERE id = @itemId"
|
||||
|> Sql.executeNonQueryAsync
|
||||
|> Async.AwaitTask
|
||||
|
||||
let restockMerris itemName amount (ctx : IDiscordContext) =
|
||||
task {
|
||||
do! Messaging.defer ctx
|
||||
if amount > 0 && amount < 1000 then
|
||||
do! updateMerrisItem itemName amount |> Async.Ignore
|
||||
do! Messaging.sendFollowUpMessage ctx $"Restocked {itemName} by {amount}"
|
||||
else
|
||||
do! Messaging.sendFollowUpMessage ctx $"Provide a restock amount between 0 and 1000"
|
||||
} :> Task
|
||||
|
||||
let getUsersFromMessageReactions (channel : DiscordChannel) (message : string) (ctx : IDiscordContext) =
|
||||
task {
|
||||
do! Messaging.defer ctx
|
||||
@ -313,12 +284,6 @@ type AdminBot() =
|
||||
member this.GetRaffleWinners (ctx : InteractionContext, [<Option("count", "How many winners to pick")>] count : int64) =
|
||||
enforceAdmin (DiscordInteractionContext ctx) (getRaffleWinners count)
|
||||
|
||||
[<SlashCommand("restock-merris", "Restock Merris Mart Item")>]
|
||||
member this.ResstockMerris (ctx : InteractionContext,
|
||||
[<Option("item-id", "Which item to restock")>] itemId : MerrisItem,
|
||||
[<Option("amount", "How much to restock by")>] amount : int64) =
|
||||
restockMerris itemId (int amount) (DiscordInteractionContext ctx)
|
||||
|
||||
// [<SlashCommand("admin-raffles-toggle", "Toggle availability of an item")>]
|
||||
// member this.ToggleRaffle (ctx : InteractionContext, [<Option("enabled", "Enable or Disable?")>] enable : EnableDisable) =
|
||||
// enforceAdmin (DiscordInteractionContext ctx) (toggleRaffleAvailability (enable = EnableDisable.Enable))
|
||||
|
@ -59,7 +59,6 @@ let roleWhitelistPending = getId "ROLE_WHITELIST_PENDING"
|
||||
let roleWhiteOGPending = getId "ROLE_WHITEOG_PENDING"
|
||||
let roleWhitelist = getId "ROLE_WHITELIST"
|
||||
let roleWhiteOG = getId "ROLE_WHITEOG"
|
||||
// let roleMod = getId "ROLE_MOD"
|
||||
let roleAdmin = getId "ROLE_ADMIN"
|
||||
let roleMagicEden = getId "ROLE_MAGICEDEN"
|
||||
let roleRecruiter1x = getId "ROLE_RECRUITER_1X"
|
||||
|
@ -1,18 +0,0 @@
|
||||
:headers = <<
|
||||
Content-Type: application/json
|
||||
X-API-Key: 1a2ff166-8985-47f0-8e5a-a7c6fb321a3f
|
||||
#
|
||||
|
||||
# Get balance
|
||||
GET https://degenz-currency-api-prod-jv56z.ondigitalocean.app/user/90588624566886400/balance
|
||||
:headers
|
||||
|
||||
# Increment balance
|
||||
PATCH https://degenz-currency-api-prod-jv56z.ondigitalocean.app/user/90588624566886400/balance/deposit
|
||||
:headers
|
||||
{ "amount" : 10 }
|
||||
|
||||
# Decrement balance
|
||||
PATCH https://degenz-currency-api-prod-jv56z.ondigitalocean.app/user/90588624566886400/balance/withdraw
|
||||
:headers
|
||||
{ "amount" : 100 }
|
149
README.md
149
README.md
@ -1,149 +0,0 @@
|
||||
# DegenzGame
|
||||
|
||||
A multiplayer Discord gaming platform built with F# and the Giraffe web framework, featuring interactive games with player progression and an in-game economy.
|
||||
|
||||
## Degenz World
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Technologies
|
||||
|
||||
- **F# (.NET 6.0)**: Primary programming language for all game logic and bot functionality
|
||||
- **Giraffe**: F# web framework used for the Currency API microservice
|
||||
- **DSharpPlus**: Discord API library for bot interactions and slash commands
|
||||
- **PostgreSQL**: Database with Npgsql.FSharp for type-safe operations
|
||||
- **Docker**: Containerized deployment
|
||||
|
||||
### Project Structure
|
||||
|
||||
The solution consists of two main projects:
|
||||
|
||||
- **Bot** (`Bot.fsproj`): Main Discord bot application containing all game implementations
|
||||
- **CurrencyAPI** (`CurrencyAPI.fsproj`): HTTP API microservice for currency balance management
|
||||
|
||||
## Game System
|
||||
|
||||
### Available Games
|
||||
|
||||
#### HackerBattle
|
||||
Cyberpunk-themed combat game where players:
|
||||
- Use hack items (VIRUS, REMOTE, WORM) to attack other players and steal GBT currency
|
||||
- Deploy shields (FIREWALL, ENCRYPTION, CYPHER) for defense
|
||||
- Items have cooldown periods and class-based effectiveness
|
||||
- Combat resolution based on item power vs resistance stats
|
||||
- Includes comprehensive validation (can't attack yourself, cooldown checks, etc.)
|
||||
|
||||
#### SlotMachine
|
||||
Casino-style slot machine featuring:
|
||||
- Custom symbols: BigBrother, Eye, Obey, AnonMask, Rat, Ramen, Sushi, Pizza
|
||||
- Configurable reel probabilities per symbol
|
||||
- Prize table with money rewards and jackpot system
|
||||
- Integration with GBT currency system
|
||||
|
||||
#### Thief Game
|
||||
Player-vs-player stealing mechanics:
|
||||
- 1-minute cooldown for thieves, 1-hour recovery for victims
|
||||
- Success chance calculation based on player stats
|
||||
- Payout formula: `defenderBank * 0.1 * (1.0 - chance)` with random bonus
|
||||
- Two outcomes: Success (steal GBT) or WentToPrison (imprisoned status)
|
||||
|
||||
#### Rock Paper Scissors
|
||||
Classic game with Discord UI integration and animated GIF results
|
||||
|
||||
#### Store System
|
||||
In-game marketplace with:
|
||||
- Fixed-price item purchasing with GBT currency
|
||||
- Stock management (limited/unlimited items)
|
||||
- Role-based access control and invite requirements
|
||||
- Timed sales with Unix timestamp endpoints
|
||||
- Stack limits for certain items
|
||||
|
||||
#### Trainer System
|
||||
Tutorial/onboarding system:
|
||||
- Grants "Trainee" role to new players
|
||||
- Multi-step tutorial teaching shield and hack mechanics
|
||||
- Provides starting items (REMOTE hack, FIREWALL shield)
|
||||
- Simulated combat against "Sensei" bot
|
||||
|
||||
### Player Progression
|
||||
|
||||
#### Stats System
|
||||
Four core attributes with time-based decay:
|
||||
- **Strength**: Combat effectiveness
|
||||
- **Focus**: Precision and accuracy
|
||||
- **Charisma**: Social interactions
|
||||
- **Luck**: Random event outcomes
|
||||
|
||||
Each stat features:
|
||||
- Base range 0-100 with item-based modifications
|
||||
- Decay rate of 2.09 (100 to 0 in ~48 hours)
|
||||
- Item effects: Min/Max bounds, Add bonuses, RateMultiplier
|
||||
|
||||
## Discord Integration
|
||||
|
||||
### Multi-Bot Architecture
|
||||
The system uses multiple specialized Discord bot clients:
|
||||
- **hackerBattleBot**: HackerBattle game interactions
|
||||
- **storeBot**: Store transactions and interactions
|
||||
- **inviterBot**: Member recruitment and invite tracking
|
||||
- **slotsBot**: Slot machine operations
|
||||
- **adminBot**: Administrative functions
|
||||
|
||||
### Event Handling
|
||||
- Component interactions (buttons, dropdowns)
|
||||
- Slash command registration per guild
|
||||
- Message creation events
|
||||
- Guild member join/update tracking
|
||||
- Real-time game state management
|
||||
|
||||
## Database Design
|
||||
|
||||
### PostgreSQL Integration
|
||||
Type-safe database operations using Npgsql.FSharp:
|
||||
|
||||
- **User table**: Discord IDs, display names, GBT balances
|
||||
- **Item system**: Database-driven item definitions with custom composite types
|
||||
- **Event tracking**: Player actions, cooldowns, game history with proper typing
|
||||
- **Custom types**: `StatMod` composite type for database item modifiers
|
||||
|
||||
## Currency API (Giraffe)
|
||||
|
||||
### Endpoints (`Currency.fs`)
|
||||
- **GET `/user/{id}/balance`**: Retrieve player's GBT balance
|
||||
- **PATCH `/user/{id}/balance/withdraw`**: Deduct GBT (with insufficient funds validation)
|
||||
- **PATCH `/user/{id}/balance/deposit`**: Add GBT to player account
|
||||
- **API Key Authentication**: `X-API-Key` header validation
|
||||
- **Error Handling**: Comprehensive error responses and logging
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
- .NET 6.0 SDK
|
||||
- PostgreSQL database
|
||||
- Discord application with multiple bot tokens
|
||||
- Paket for F# package management
|
||||
|
||||
### Configuration
|
||||
Environment variables required:
|
||||
- `DATABASE_URL`: PostgreSQL connection string
|
||||
- `API_KEY`: Currency API authentication key
|
||||
- Multiple Discord bot tokens for specialized bots
|
||||
|
||||
### Running the Application
|
||||
```bash
|
||||
# Restore packages
|
||||
dotnet paket restore
|
||||
|
||||
# Build the solution
|
||||
dotnet build
|
||||
|
||||
# Run the Discord bot
|
||||
dotnet run --project Bot
|
||||
|
||||
# Run the Currency API
|
||||
dotnet run --project CurrencyAPI
|
||||
```
|
@ -5,9 +5,9 @@ framework: net6.0, netstandard2.0, netstandard2.1
|
||||
|
||||
nuget FSharp.Core >= 6.0.0
|
||||
|
||||
nuget DSharpPlus >= 4.3.0-nightly-01160
|
||||
nuget DSharpPlus.Interactivity >= 4.3.0-nightly-01160
|
||||
nuget DSharpPlus.SlashCommands >= 4.3.0-nightly-01160
|
||||
nuget DSharpPlus >= 4.3.0-nightly-01135
|
||||
nuget DSharpPlus.Interactivity >= 4.3.0-nightly-01135
|
||||
nuget DSharpPlus.SlashCommands >= 4.3.0-nightly-01135
|
||||
nuget FSharp.Data
|
||||
nuget FsToolkit.ErrorHandling
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user