mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-04-30 03:22:04 -04:00
534 lines
22 KiB
C#
534 lines
22 KiB
C#
using System.Text.Json;
|
|
using KfChatDotNetBot.Extensions;
|
|
using KfChatDotNetBot.Models;
|
|
using KfChatDotNetBot.Models.DbModels;
|
|
using KfChatDotNetBot.Settings;
|
|
using NLog;
|
|
using StackExchange.Redis;
|
|
using System.Text.Json.Serialization;
|
|
using KfChatDotNetBot.Commands.Kasino;
|
|
|
|
namespace KfChatDotNetBot.Services;
|
|
|
|
public class KasinoMines
|
|
{
|
|
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
|
private IDatabase? _redisDb;
|
|
private static ChatBot _kfChatBot = null!;
|
|
public Dictionary<int, KasinoMinesGame> ActiveGames = new();
|
|
private decimal HOUSE_EDGE = (decimal)0.98; //used to rig win rate, payout is 100% fair. with shop i plan to implement a sort of kasino shop profile holding the investments and buffs and tracking the gamblers current house edge
|
|
public class KasinoMinesGame
|
|
{
|
|
public GamblerDbModel Creator { get; set; }
|
|
public DateTimeOffset LastInteracted = DateTimeOffset.UtcNow;
|
|
|
|
public char[][] MinesBoard;
|
|
public decimal Wager { get; set; }
|
|
public int Size { get; set; }
|
|
public int Mines { get; set; }
|
|
public List<(int r, int c)> BetsPlaced;
|
|
public string? LastMessageId;
|
|
public string LastMessageReference = "";
|
|
|
|
|
|
public KasinoMinesGame(GamblerDbModel creator, decimal wager, int size, int mines)
|
|
{
|
|
Creator = creator;
|
|
Size = size;
|
|
Mines = mines;
|
|
Wager = wager;
|
|
MinesBoard = CreateBoard();
|
|
BetsPlaced = [];
|
|
}
|
|
|
|
public async Task ResetMessage(SentMessageTrackerModel msg)
|
|
{
|
|
_logger.Info("Resetting message");
|
|
// 0 is the default for int
|
|
if (LastMessageId != null)
|
|
{
|
|
await _kfChatBot.KfClient.DeleteMessageAsync(LastMessageId);
|
|
}
|
|
|
|
LastMessageId = msg.ChatMessageUuid ?? throw new InvalidOperationException($"ChatMessageId was null for {msg.Reference}");
|
|
LastMessageReference = msg.Reference;
|
|
}
|
|
|
|
public async Task RigBoard((int r, int c) coord) //moves one of the mines to a specified coordinate for house edge rigging
|
|
{
|
|
//find the first mine
|
|
(int r, int c) originalMine = (11, 11);
|
|
bool brek = false;
|
|
for (int r = 0; r < Size; r++)
|
|
{
|
|
for (int c = 0; c < Size; c++)
|
|
{
|
|
if (MinesBoard[r][ c] == 'M')
|
|
{
|
|
originalMine = (r, c);
|
|
brek = true;
|
|
break;
|
|
}
|
|
}
|
|
if (brek) break;
|
|
}
|
|
|
|
MinesBoard[coord.r][ coord.c] = 'M';
|
|
if (originalMine.r == 11)
|
|
{
|
|
_logger.Error("Rigboard failed to find a mine somehow?");
|
|
return;
|
|
}
|
|
MinesBoard[originalMine.r][ originalMine.c] = 'G';
|
|
|
|
}
|
|
public async Task Explode((int r, int c) mineLocation, SentMessageTrackerModel msg)
|
|
{
|
|
if (LastMessageId == null || LastMessageId != msg.ChatMessageUuid)
|
|
{
|
|
await ResetMessage(msg);
|
|
}
|
|
|
|
int frames = 16;
|
|
string str;
|
|
bool revealedSpace;
|
|
int yellowWave = 1;
|
|
int orangeWave = 2;
|
|
int redWave = 3;
|
|
int whiteWave = 0;
|
|
for (int f = 0; f < frames; f++)
|
|
{
|
|
str = "";
|
|
for (int r = 0; r < Size; r++)
|
|
{
|
|
await Task.Delay(100);
|
|
|
|
for (int c = 0; c < Size; c++)
|
|
{
|
|
revealedSpace = false;
|
|
foreach (var bet in BetsPlaced)
|
|
{
|
|
if (bet.r == r && bet.c == c) revealedSpace = true;
|
|
}
|
|
|
|
if (mineLocation.r == r && mineLocation.c == c)
|
|
{
|
|
str += "💣";
|
|
}
|
|
else if (revealedSpace)
|
|
{
|
|
str += "💎";
|
|
}
|
|
else if (DistanceFromMine((r, c)).vertical == yellowWave || DistanceFromMine((r, c)).horizontal == yellowWave)
|
|
{
|
|
str += "🟨";
|
|
}
|
|
else if (DistanceFromMine((r, c)).vertical == orangeWave ||
|
|
DistanceFromMine((r, c)).horizontal == orangeWave)
|
|
{
|
|
str += "🟧";
|
|
}
|
|
else if (DistanceFromMine((r, c)).vertical == redWave ||
|
|
DistanceFromMine((r, c)).horizontal == redWave)
|
|
{
|
|
str += "🟥";
|
|
}
|
|
else if (DistanceFromMine((r, c)).vertical == whiteWave ||
|
|
DistanceFromMine((r, c)).horizontal == whiteWave)
|
|
{
|
|
str += "⬜";
|
|
}
|
|
else
|
|
{
|
|
str += "⬜";
|
|
}
|
|
}
|
|
str += "[br]";
|
|
}
|
|
|
|
yellowWave++;
|
|
orangeWave++;
|
|
redWave++;
|
|
whiteWave++;
|
|
await Task.Delay(100);
|
|
await _kfChatBot.KfClient.EditMessageAsync(LastMessageId!, $"{str}[br]{Creator.User.FormatUsername()}");
|
|
}
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(10));
|
|
await _kfChatBot.KfClient.DeleteMessageAsync(msg.ChatMessageUuid!);
|
|
LastMessageId = null;
|
|
|
|
(int vertical, int horizontal) DistanceFromMine((int r, int c) coord)
|
|
{
|
|
return (Math.Abs(coord.r - mineLocation.r), Math.Abs(coord.c - mineLocation.c));
|
|
}
|
|
}
|
|
|
|
public new string ToString()
|
|
{
|
|
string value = "";
|
|
bool revealedSpace;
|
|
for (int r = 0; r < Size; r++)
|
|
{
|
|
for (int c = 0; c < Size; c++)
|
|
{
|
|
revealedSpace = false;
|
|
foreach (var bet in BetsPlaced)
|
|
{
|
|
if (bet.r == r && bet.c == c) revealedSpace = true;
|
|
}
|
|
|
|
if (!revealedSpace)
|
|
{
|
|
value += "⬜";
|
|
}
|
|
else if (MinesBoard[r][ c] == 'M') value += "💣";
|
|
else value += "💎";
|
|
}
|
|
|
|
value += "[br]";
|
|
}
|
|
|
|
value += $"{Creator.User.FormatUsername()} [ditto]!mines 1[/ditto] [ditto]!mines cashout[/ditto]";
|
|
return value;
|
|
}
|
|
|
|
public char[][] CreateBoard()
|
|
{
|
|
char[][] board = new char[Size][];
|
|
for (int i = 0; i < Size; i++) board[i] = new char[Size];
|
|
|
|
List<(int r, int c)> minesCoords = new List<(int r, int c)>();
|
|
(int r, int c) coord;
|
|
int counter = 0;
|
|
bool gems = !(Mines < (Size * Size)/2); //if there are more mines than gems, generate list of gem locations instead since thats less generations
|
|
int coordsCounter;
|
|
if (gems) coordsCounter = Size * Size - Mines;
|
|
else coordsCounter = Mines;
|
|
while (minesCoords.Count != coordsCounter)
|
|
{
|
|
coord = (Money.GetRandomNumber(Creator, 0, Size, incrementMaxParam: false), Money.GetRandomNumber(Creator, 0, Size, incrementMaxParam: false));
|
|
if (!minesCoords.Contains(coord)) minesCoords.Add(coord);
|
|
else counter++;
|
|
if (counter >= 100000) throw new Exception($"mines failed to generate mines coordinates. Mines: {Mines} | Board size: {Size} | Current count of mines list {minesCoords.Count}");
|
|
}
|
|
|
|
foreach (var coords in minesCoords)
|
|
{
|
|
if (gems) board[coords.r][ coords.c] = 'G';
|
|
else board[coords.r][ coords.c] = 'M';
|
|
}
|
|
for (int r = 0; r < Size; r++)
|
|
{
|
|
for (int c = 0; c < Size; c++)
|
|
{
|
|
if (gems)
|
|
{
|
|
if (!(board[r][c] == 'G')) board[r][ c] = 'M';
|
|
}
|
|
else
|
|
{
|
|
if (!(board[r][c] == 'M')) board[r][ c] = 'G';
|
|
}
|
|
}
|
|
}
|
|
|
|
return board;
|
|
}
|
|
|
|
}
|
|
|
|
public KasinoMines(ChatBot kfChatBot, int gamblerId)
|
|
{
|
|
_kfChatBot = kfChatBot;
|
|
if (!Redis.IsAvailable)
|
|
{
|
|
_logger.Error($"Can't initialize the Kasino Mines service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
|
|
$"or the Redis service failed to connect");
|
|
return;
|
|
}
|
|
|
|
_redisDb = Redis.Multiplexer.GetDatabase();
|
|
GetSavedGames(gamblerId).Wait();
|
|
}
|
|
|
|
public async Task RefreshGameMessage(int gamblerId)
|
|
{
|
|
await GetSavedGames(gamblerId);
|
|
var game = ActiveGames[gamblerId];
|
|
game.LastInteracted = DateTimeOffset.UtcNow;
|
|
var msg = await _kfChatBot.SendChatMessageAsync($"{game.ToString()}", true);
|
|
await _kfChatBot.WaitForChatMessageAsync(msg);
|
|
await game.ResetMessage(msg);
|
|
ActiveGames[gamblerId] = game;
|
|
await SaveActiveGames(gamblerId);
|
|
}
|
|
|
|
public async Task GetSavedGames(int gamblerId)
|
|
{
|
|
if (_redisDb == null) throw new InvalidOperationException("Kasino mines service isn't initialized");
|
|
var json = await _redisDb.StringGetAsync($"Mines.State.{gamblerId}");
|
|
if (string.IsNullOrEmpty(json)) return;
|
|
try
|
|
{
|
|
var options = new JsonSerializerOptions{IncludeFields = true};
|
|
ActiveGames = JsonSerializer.Deserialize<Dictionary<int, KasinoMinesGame>>(json.ToString(), options) ??
|
|
throw new InvalidOperationException();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.Error(e);
|
|
_logger.Error("Potentially failed to deserialize active mines games in GetSavedGames() in KasinoMines in Services");
|
|
ActiveGames = new Dictionary<int, KasinoMinesGame>();
|
|
}
|
|
}
|
|
public async Task SaveActiveGames(int gamblerId)
|
|
{
|
|
if (_redisDb == null) throw new InvalidOperationException("Kasino mines service isn't initialized");
|
|
var options = new JsonSerializerOptions
|
|
{
|
|
IncludeFields = true,
|
|
WriteIndented = false
|
|
};
|
|
var json = JsonSerializer.Serialize(ActiveGames, options);
|
|
await _redisDb.StringSetAsync($"Mines.State.{gamblerId}", json, null, When.Always);
|
|
}
|
|
|
|
public async Task RemoveGame(int gamblerId)
|
|
{
|
|
await GetSavedGames(gamblerId);
|
|
//attempt to delete the message if its there
|
|
if (ActiveGames[gamblerId].LastMessageId != null) await _kfChatBot.KfClient.DeleteMessageAsync(ActiveGames[gamblerId].LastMessageId!);
|
|
ActiveGames.Remove(gamblerId);
|
|
await SaveActiveGames(gamblerId);
|
|
}
|
|
|
|
public async Task Cashout(KasinoMinesGame game)
|
|
{
|
|
decimal payout = game.Wager;
|
|
decimal mines = game.Mines;
|
|
decimal size2 = game.Size * game.Size;
|
|
decimal gems = size2 - mines;
|
|
for (int i = 0; i < game.BetsPlaced.Count; i++)
|
|
{
|
|
payout *= ((size2 - i) / (gems-i));
|
|
}
|
|
//edit the message with the board to show all the mine locations if you won 💣💎⬜
|
|
string str = "";
|
|
for (int r = 0; r < game.Size; r++)
|
|
{
|
|
for (int c = 0; c < game.Size; c++)
|
|
{
|
|
bool revealedSpace = false;
|
|
foreach (var bet in game.BetsPlaced)
|
|
{
|
|
if (bet.r == r && bet.c == c) revealedSpace = true;
|
|
}
|
|
if (game.MinesBoard[r][ c] == 'M') str += "💣";
|
|
else if (revealedSpace) str += "💎";
|
|
else str += "⬜";
|
|
|
|
}
|
|
str += "[br]";
|
|
}
|
|
await _kfChatBot.KfClient.EditMessageAsync(game.LastMessageId!, str);
|
|
var net = payout - game.Wager;
|
|
var newBalance = await Money.NewWagerAsync(game.Creator.Id, game.Wager, net, WagerGame.Mines);
|
|
|
|
await _kfChatBot.SendChatMessageAsync(
|
|
$"{game.Creator.User.FormatUsername()}, you won {await payout.FormatKasinoCurrencyAsync()} from your {await game.Wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.BetsPlaced.Count} gems while avoiding {game.Mines} mines. Net: {await net.FormatKasinoCurrencyAsync()}. Balance: {await newBalance.FormatKasinoCurrencyAsync()}", true, autoDeleteAfter: TimeSpan.FromSeconds(15));
|
|
await Task.Delay(TimeSpan.FromSeconds(15));
|
|
await RemoveGame(game.Creator.Id);
|
|
}
|
|
|
|
public async Task<bool> Bet(GamblerDbModel gambler, int count, SentMessageTrackerModel msg, bool cashOut) //returns false if you hit a bomb, true if you didn't
|
|
{
|
|
int gamblerId = gambler.Id;
|
|
await GetSavedGames(gamblerId);
|
|
var game = ActiveGames[gamblerId];
|
|
game.LastInteracted = DateTimeOffset.UtcNow;
|
|
if (game.LastMessageId == null || game.LastMessageId != msg.ChatMessageUuid)
|
|
{
|
|
|
|
await game.ResetMessage(msg);
|
|
}
|
|
List<(int r, int c)> betCoords = new();
|
|
List<(int r, int c)> validBets = new();
|
|
int numGems = 0;
|
|
|
|
//first get a list of valid coordinates that could be bet on (any unrevealed tile - gem or mine)
|
|
for (int r = 0; r < game.Size; r++)
|
|
{
|
|
for (int c = 0; c < game.Size; c++)
|
|
{
|
|
if (!game.BetsPlaced.Contains((r, c)))
|
|
{
|
|
validBets.Add((r, c));
|
|
if (game.MinesBoard[r][c] == 'G') numGems++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (validBets.Count == 0)
|
|
{
|
|
await _kfChatBot.SendChatMessageAsync($"{game.Creator.User.FormatUsername()}, unable to generate valid betting positions, game closed.", true, autoDeleteAfter: TimeSpan.FromSeconds(5));
|
|
return false;
|
|
}
|
|
|
|
if (count > numGems)
|
|
{
|
|
count = numGems;
|
|
await _kfChatBot.SendChatMessageAsync(
|
|
$"{game.Creator.User.FormatUsername()}, there are only {numGems} gems left, so you bet on {count} gems, and will automatically cash out if you win.",
|
|
true, autoDeleteAfter: TimeSpan.FromSeconds(5));
|
|
cashOut = true;
|
|
}
|
|
else if (count == numGems && cashOut == false)
|
|
{
|
|
await _kfChatBot.SendChatMessageAsync($"{game.Creator.User.FormatUsername()}, you bet on all gems, so you will automatically cash out if you win.", true, autoDeleteAfter: TimeSpan.FromSeconds(5));
|
|
cashOut = true;
|
|
}
|
|
|
|
|
|
//randomly pull from that list to add coordinates to bet on
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int rand = Money.GetRandomNumber(game.Creator, 0, validBets.Count - 1);
|
|
betCoords.Add(validBets[rand]);
|
|
validBets.RemoveAt(rand);
|
|
}
|
|
|
|
return await Bet(gambler, betCoords, msg, cashOut, true);
|
|
}
|
|
|
|
public async Task<bool> Bet(GamblerDbModel gambler, List<(int r, int c)> coords, SentMessageTrackerModel msg, bool cashOut, bool calledFromBet = false)
|
|
{
|
|
//KasinoShop stuff -------------------------------------------------------------------------
|
|
if (_kfChatBot.BotServices.KasinoShop != null)
|
|
{
|
|
await GlobalShopFunctions.CheckProfile(_kfChatBot, gambler.User, gambler);
|
|
HOUSE_EDGE += _kfChatBot.BotServices.KasinoShop.Gambler_Profiles[gambler.User.KfId].HouseEdgeModifier;
|
|
}
|
|
//------------------------------------------------------------------------------------------
|
|
int gamblerId = gambler.Id;
|
|
await GetSavedGames(gamblerId);
|
|
var game = ActiveGames[gamblerId];
|
|
game.LastInteracted = DateTimeOffset.UtcNow;
|
|
if (game.LastMessageId == null || game.LastMessageId != msg.ChatMessageUuid)
|
|
{
|
|
await game.ResetMessage(msg);
|
|
}
|
|
|
|
List<(int r, int c)> bets = new();
|
|
if (!calledFromBet)
|
|
{
|
|
List<(int r, int c)> validBets = new();
|
|
int numGems = 0;
|
|
|
|
//first get a list of valid coordinates that could be bet on (any unrevealed cell for user-provided coords)
|
|
for (int r = 0; r < game.Size; r++)
|
|
{
|
|
for (int c = 0; c < game.Size; c++)
|
|
{
|
|
if (!game.BetsPlaced.Contains((r, c)))
|
|
{
|
|
validBets.Add((r, c));
|
|
if (game.MinesBoard[r][c] == 'G') numGems++;
|
|
}
|
|
}
|
|
}
|
|
|
|
var invalidBetMsg = await _kfChatBot.SendChatMessageAsync($"{game.Creator.User.FormatUsername()}, checking bets...", true);
|
|
await _kfChatBot.WaitForChatMessageAsync(invalidBetMsg);
|
|
foreach (var bet in coords)
|
|
{
|
|
if (!validBets.Contains(bet) || game.BetsPlaced.Contains(bet) || bets.Contains(bet))
|
|
{
|
|
await _kfChatBot.KfClient.EditMessageAsync(invalidBetMsg.ChatMessageUuid!,
|
|
$"{game.Creator.User.FormatUsername()}, invalid bet of {bet.r},{bet.c} removed (already placed, duplicate, or invalid coordinate)");
|
|
await Task.Delay(5);
|
|
}
|
|
else bets.Add(bet);
|
|
}
|
|
|
|
|
|
if (bets.Count > numGems)
|
|
{
|
|
|
|
await _kfChatBot.KfClient.EditMessageAsync(invalidBetMsg.ChatMessageUuid!,
|
|
$"{game.Creator.User.FormatUsername()}, you bet on {bets.Count} gems, but there are only {numGems} left. Your list of bets was automatically truncated, and the game will automatically cash out if you win.");
|
|
bets.RemoveRange(numGems, bets.Count - numGems);
|
|
cashOut = true;
|
|
}
|
|
else if (bets.Count == numGems)
|
|
{
|
|
await _kfChatBot.KfClient.EditMessageAsync(invalidBetMsg.ChatMessageUuid!,
|
|
$"{game.Creator.User.FormatUsername()}, you bet on all gems, so you will automatically cash out if you win.");
|
|
cashOut = true;
|
|
}
|
|
|
|
await Task.Delay(50);
|
|
_ = _kfChatBot.KfClient.DeleteMessageAsync(invalidBetMsg.ChatMessageUuid!);
|
|
|
|
}
|
|
else bets = coords;
|
|
foreach (var coord in bets) //the main portion of the game
|
|
{
|
|
var r = Money.GetRandomNumber(game.Creator, 0, 100);
|
|
await Task.Delay(100);
|
|
if (game.MinesBoard[coord.r][ coord.c] == 'M')
|
|
{
|
|
game.BetsPlaced.Add(coord);
|
|
await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, game.ToString());
|
|
_ = game.Explode((coord.r, coord.c), msg);
|
|
var newBalance = await Money.NewWagerAsync(game.Creator.Id, game.Wager, -game.Wager, WagerGame.Mines);
|
|
var net = -game.Wager;
|
|
await _kfChatBot.SendChatMessageAsync(
|
|
$"{game.Creator.User.FormatUsername()}, you lost your {await game.Wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.BetsPlaced.Count} gems until you hit one of {game.Mines} mines. Net: {await net.FormatKasinoCurrencyAsync()}. Balance: {await newBalance.FormatKasinoCurrencyAsync()}",
|
|
true, autoDeleteAfter: TimeSpan.FromSeconds(15));
|
|
await RemoveGame(gamblerId);
|
|
return false;
|
|
}
|
|
|
|
if (r > 100 * HOUSE_EDGE && game.BetsPlaced.Count == 0)//if you didn't lose, check to see if the switch was flipped (only on the first round)
|
|
{
|
|
game.BetsPlaced.Add(coord);
|
|
await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, game.ToString());
|
|
await game.RigBoard(coord);
|
|
await Task.Delay(75);
|
|
await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, game.ToString());
|
|
_ = game.Explode(coord, msg);
|
|
var newBalance = await Money.NewWagerAsync(game.Creator.Id, game.Wager, -game.Wager, WagerGame.Mines);
|
|
var net = -game.Wager;
|
|
await _kfChatBot.SendChatMessageAsync(
|
|
$"R! {game.Creator.User.FormatUsername()}, you lost your {await game.Wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.BetsPlaced.Count} gems until you hit one of {game.Mines} mines. Net: {await net.FormatKasinoCurrencyAsync()}. Balance: {await newBalance.FormatKasinoCurrencyAsync()}",
|
|
true, autoDeleteAfter: TimeSpan.FromSeconds(15));
|
|
await RemoveGame(gamblerId);
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
game.BetsPlaced.Add(coord);
|
|
}
|
|
await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, game.ToString());
|
|
}
|
|
|
|
|
|
ActiveGames[gamblerId] = game;
|
|
if (cashOut) await Cashout(game);
|
|
else await SaveActiveGames(gamblerId);
|
|
return true;
|
|
}
|
|
|
|
public async Task CreateGame(GamblerDbModel gambler, decimal bet, int size, int mines)
|
|
{
|
|
await GetSavedGames(gambler.Id);
|
|
ActiveGames.Add(gambler.Id, new KasinoMinesGame(gambler, bet, size, mines));
|
|
await SaveActiveGames(gambler.Id);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|