added .rakeback to get a part of the house edge back. Rakeback is accumulated by betting (not winning or losing in particular). All games have manually specified rakeback values

slot now has 1 more icon (wheat!), and multipliers have been modified to even out the gains
betroll is improved (around 2% better payout), as 66 is now a winning number, not a losing one
This commit is contained in:
Toastie (DCS Team) 2024-11-07 18:28:18 +13:00
parent 14ac3c92bb
commit 66870f6859
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
21 changed files with 7282 additions and 55 deletions

View file

@ -74,6 +74,13 @@ public abstract class EllieContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
#region Rakeback
modelBuilder.Entity<Rakeback>()
.HasKey(x => x.UserId);
#endregion
#region UserBetStats #region UserBetStats
modelBuilder.Entity<UserBetStats>() modelBuilder.Entity<UserBetStats>()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class rakeback : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rakeback",
columns: table => new
{
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
amount = table.Column<decimal>(type: "numeric", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_rakeback", x => x.userid);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rakeback");
}
}
}

View file

@ -3227,6 +3227,23 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("greetsettings", (string)null); b.ToTable("greetsettings", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Rakeback", b =>
{
b.Property<decimal>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<decimal>("Amount")
.HasColumnType("numeric")
.HasColumnName("amount");
b.HasKey("UserId")
.HasName("pk_rakeback");
b.ToTable("rakeback", (string)null);
});
modelBuilder.Entity("EllieBot.Services.UserBetStats", b => modelBuilder.Entity("EllieBot.Services.UserBetStats", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class rakeback : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Rakeback",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Amount = table.Column<decimal>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Rakeback", x => x.UserId);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Rakeback");
}
}
}

View file

@ -2399,6 +2399,20 @@ namespace EllieBot.Migrations
b.ToTable("GreetSettings"); b.ToTable("GreetSettings");
}); });
modelBuilder.Entity("EllieBot.Services.Rakeback", b =>
{
b.Property<ulong>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal>("Amount")
.HasColumnType("TEXT");
b.HasKey("UserId");
b.ToTable("Rakeback");
});
modelBuilder.Entity("EllieBot.Services.UserBetStats", b => modelBuilder.Entity("EllieBot.Services.UserBetStats", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

View file

@ -38,6 +38,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly IRemindService _remind; private readonly IRemindService _remind;
private readonly GamblingTxTracker _gamblingTxTracker; private readonly GamblingTxTracker _gamblingTxTracker;
private readonly IPatronageService _ps; private readonly IPatronageService _ps;
private readonly RakebackService _rb;
public Gambling( public Gambling(
IGamblingService gs, IGamblingService gs,
@ -50,7 +51,8 @@ public partial class Gambling : GamblingModule<GamblingService>
IBankService bank, IBankService bank,
IRemindService remind, IRemindService remind,
IPatronageService patronage, IPatronageService patronage,
GamblingTxTracker gamblingTxTracker) GamblingTxTracker gamblingTxTracker,
RakebackService rb)
: base(configService) : base(configService)
{ {
_gs = gs; _gs = gs;
@ -60,6 +62,7 @@ public partial class Gambling : GamblingModule<GamblingService>
_bank = bank; _bank = bank;
_remind = remind; _remind = remind;
_gamblingTxTracker = gamblingTxTracker; _gamblingTxTracker = gamblingTxTracker;
_rb = rb;
_ps = patronage; _ps = patronage;
_rng = new EllieRandom(); _rng = new EllieRandom();
@ -318,7 +321,6 @@ public partial class Gambling : GamblingModule<GamblingService>
var val = Config.Timely.Amount; var val = Config.Timely.Amount;
var boostGuilds = Config.BoostBonus.GuildIds ?? new(); var boostGuilds = Config.BoostBonus.GuildIds ?? new();
var guildUsers = await boostGuilds var guildUsers = await boostGuilds
.Select(async gid => .Select(async gid =>
@ -1089,4 +1091,45 @@ public partial class Gambling : GamblingModule<GamblingService>
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%") footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%")
.SendAsync(); .SendAsync();
} }
private EllieInteractionBase CreateRakebackInteraction()
=> _inter.Create(ctx.User.Id,
new ButtonBuilder(
customId: "cash:rakeback",
emote: new Emoji("💸")),
RakebackAction);
private async Task RakebackAction(SocketMessageComponent arg)
{
var rb = await _rb.ClaimRakebackAsync(ctx.User.Id);
if (rb == 0)
{
await arg.DeferAsync();
return;
}
await arg.RespondAsync(_sender, GetText(strs.rakeback_claimed(N(rb))), MsgType.Ok);
}
[Cmd]
public async Task Rakeback()
{
var rb = await _rb.GetRakebackAsync(ctx.User.Id);
if (rb < 1)
{
await Response()
.Error(strs.rakeback_none)
.SendAsync();
return;
}
var inter = CreateRakebackInteraction();
await Response()
.Pending(strs.rakeback_available(N(rb)))
.Interaction(inter)
.SendAsync();
}
} }

View file

@ -11,7 +11,7 @@ namespace EllieBot.Modules.Gambling.Common;
public sealed partial class GamblingConfig : ICloneable<GamblingConfig> public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
{ {
[Comment("""DO NOT CHANGE""")] [Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 11; public int Version { get; set; } = 12;
[Comment("""Currency settings""")] [Comment("""Currency settings""")]
public CurrencyConfig Currency { get; set; } public CurrencyConfig Currency { get; set; }
@ -164,7 +164,7 @@ public partial class BetRollConfig
}, },
new() new()
{ {
WhenAbove = 66, WhenAbove = 65,
MultiplyBy = 2 MultiplyBy = 2
} }
]; ];
@ -226,7 +226,7 @@ public partial class LuckyLadderSettings
public decimal[] Multipliers { get; set; } public decimal[] Multipliers { get; set; }
public LuckyLadderSettings() public LuckyLadderSettings()
=> Multipliers = [2.4M, 1.7M, 1.5M, 1.2M, 0.5M, 0.3M, 0.2M, 0.1M]; => Multipliers = [2.4M, 1.7M, 1.5M, 1.1M, 0.5M, 0.3M, 0.2M, 0.1M];
} }
[Cloneable] [Cloneable]

View file

@ -189,11 +189,16 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
}); });
} }
if (data.Version < 11) if (data.Version < 12)
{ {
ModifyConfig(c => ModifyConfig(c =>
{ {
c.Version = 11; c.Version = 12;
if (c.BetRoll.Pairs.Length == 3 && c.BetRoll.Pairs[2].WhenAbove == 66)
{
c.BetRoll.Pairs[2].WhenAbove = 65;
}
}); });
} }
} }

View file

@ -13,5 +13,10 @@ public interface IGamblingService
Task<OneOf<SlotResult, GamblingError>> SlotAsync(ulong userId, long amount); Task<OneOf<SlotResult, GamblingError>> SlotAsync(ulong userId, long amount);
Task<FlipResult[]> FlipAsync(int count); Task<FlipResult[]> FlipAsync(int count);
Task<OneOf<RpsResult, GamblingError>> RpsAsync(ulong userId, long amount, byte pick); Task<OneOf<RpsResult, GamblingError>> RpsAsync(ulong userId, long amount, byte pick);
Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(ulong userId, long amount, byte? maybeGuessValue, byte? maybeGuessColor);
Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(
ulong userId,
long amount,
byte? maybeGuessValue,
byte? maybeGuessColor);
} }

View file

@ -1,4 +1,6 @@
#nullable disable #nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Modules.Gambling.Betdraw; using EllieBot.Modules.Gambling.Betdraw;
using EllieBot.Modules.Gambling.Rps; using EllieBot.Modules.Gambling.Rps;
using EllieBot.Modules.Gambling.Services; using EllieBot.Modules.Gambling.Services;
@ -8,12 +10,12 @@ namespace EllieBot.Modules.Gambling;
public sealed class NewGamblingService : IGamblingService, IEService public sealed class NewGamblingService : IGamblingService, IEService
{ {
private readonly GamblingConfigService _bcs; private readonly GamblingConfigService _gcs;
private readonly ICurrencyService _cs; private readonly ICurrencyService _cs;
public NewGamblingService(GamblingConfigService bcs, ICurrencyService cs) public NewGamblingService(GamblingConfigService gcs, ICurrencyService cs)
{ {
_bcs = bcs; _gcs = gcs;
_cs = cs; _cs = cs;
} }
@ -31,7 +33,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
} }
} }
var game = new LulaGame(_bcs.Data.LuckyLadder.Multipliers); var game = new LulaGame(_gcs.Data.LuckyLadder.Multipliers);
var result = game.Spin(amount); var result = game.Spin(amount);
var won = (long)result.Won; var won = (long)result.Won;
@ -57,9 +59,9 @@ public sealed class NewGamblingService : IGamblingService, IEService
} }
} }
var game = new BetrollGame(_bcs.Data.BetRoll.Pairs var game = new BetrollGame(_gcs.Data.BetRoll.Pairs
.Select(x => (x.WhenAbove, (decimal)x.MultiplyBy)) .Select(x => (x.WhenAbove, (decimal)x.MultiplyBy))
.ToList()); .ToList());
var result = game.Roll(amount); var result = game.Roll(amount);
@ -88,7 +90,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
} }
} }
var game = new BetflipGame(_bcs.Data.BetFlip.Multiplier); var game = new BetflipGame(_gcs.Data.BetFlip.Multiplier);
var result = game.Flip(guess, amount); var result = game.Flip(guess, amount);
var won = (long)result.Won; var won = (long)result.Won;
@ -100,7 +102,11 @@ public sealed class NewGamblingService : IGamblingService, IEService
return result; return result;
} }
public async Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(ulong userId, long amount, byte? maybeGuessValue, byte? maybeGuessColor) public async Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(
ulong userId,
long amount,
byte? maybeGuessValue,
byte? maybeGuessColor)
{ {
ArgumentOutOfRangeException.ThrowIfNegative(amount); ArgumentOutOfRangeException.ThrowIfNegative(amount);
@ -266,3 +272,45 @@ public sealed class NewGamblingService : IGamblingService, IEService
return result; return result;
} }
} }
public sealed class RakebackService : IEService
{
private readonly DbService _db;
private readonly ICurrencyService _cs;
public RakebackService(DbService db, ICurrencyService cs)
{
_db = db;
_cs = cs;
}
public async Task<long> GetRakebackAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var rb = uow.GetTable<Rakeback>()
.Where(x => x.UserId == userId)
.Select(x => x.Amount)
.FirstOrDefault();
return (long)rb;
}
public async Task<long> ClaimRakebackAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var rbs = await uow.GetTable<Rakeback>()
.Where(x => x.UserId == userId)
.DeleteWithOutputAsync((x) => x.Amount);
if (rbs.Length == 0)
return 0;
var rb = (long)rbs[0];
await _cs.AddAsync(userId, rb, new("rakeback", "claim"));
return rb;
}
}

View file

@ -11,9 +11,9 @@ public class SlotGame
{ {
var rolls = new[] var rolls = new[]
{ {
(byte)_rng.Next(0, 6), (byte)_rng.Next(0, 7),
(byte)_rng.Next(0, 6), (byte)_rng.Next(0, 7),
(byte)_rng.Next(0, 6) (byte)_rng.Next(0, 7)
}; };
ref var a = ref rolls[0]; ref var a = ref rolls[0];
@ -24,24 +24,24 @@ public class SlotGame
var winType = SlotWinType.None; var winType = SlotWinType.None;
if (a == b && b == c) if (a == b && b == c)
{ {
if (a == 5) if (a == 6)
{ {
winType = SlotWinType.TrippleJoker; winType = SlotWinType.TrippleJoker;
multi = 30; multi = 25;
} }
else else
{ {
winType = SlotWinType.TrippleNormal; winType = SlotWinType.TrippleNormal;
multi = 10; multi = 15;
} }
} }
else if (a == 5 && (b == 5 || c == 5) else if (a == 6 && (b == 6 || c == 6)
|| (b == 5 && c == 5)) || (b == 6 && c == 6))
{ {
winType = SlotWinType.DoubleJoker; winType = SlotWinType.DoubleJoker;
multi = 4; multi = 6;
} }
else if (a == 5 || b == 5 || c == 5) else if (a == 6 || b == 6 || c == 6)
{ {
winType = SlotWinType.SingleJoker; winType = SlotWinType.SingleJoker;
multi = 1; multi = 1;

View file

@ -8,7 +8,7 @@ namespace EllieBot.Common;
public partial class ImageUrls : ICloneable<ImageUrls> public partial class ImageUrls : ICloneable<ImageUrls>
{ {
[Comment("DO NOT CHANGE")] [Comment("DO NOT CHANGE")]
public int Version { get; set; } = 5; public int Version { get; set; } = 6;
public CoinData Coins { get; set; } public CoinData Coins { get; set; }
public Uri[] Currency { get; set; } public Uri[] Currency { get; set; }

View file

@ -88,6 +88,10 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
if (users.Count == 0) if (users.Count == 0)
continue; continue;
// rakeback
var rakebacks = new Dictionary<ulong, decimal>();
// update userstats
foreach (var (k, x) in users.GroupBy(x => (x.UserId, x.Game)) foreach (var (k, x) in users.GroupBy(x => (x.UserId, x.Game))
.ToDictionary(x => x.Key, .ToDictionary(x => x.Key,
x => x.Aggregate((a, b) => new() x => x.Aggregate((a, b) => new()
@ -100,6 +104,10 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
MaxWin = Math.Max(a.MaxWin, b.MaxWin), MaxWin = Math.Max(a.MaxWin, b.MaxWin),
}))) })))
{ {
rakebacks.TryAdd(k.UserId, 0m);
rakebacks[k.UserId] += x.TotalBet * GetHouseEdge(k.Game) * BASE_RAKEBACK;
// bulk upsert in the future // bulk upsert in the future
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.GetTable<UserBetStats>() await uow.GetTable<UserBetStats>()
@ -129,6 +137,25 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
Game = k.Game Game = k.Game
}); });
} }
foreach (var (k, v) in rakebacks)
{
await _db.GetDbContext()
.GetTable<Rakeback>()
.InsertOrUpdateAsync(() => new()
{
UserId = k,
Amount = v
},
(old) => new()
{
Amount = old.Amount + v
},
() => new()
{
UserId = k
});
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -137,6 +164,8 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
} }
} }
private const decimal BASE_RAKEBACK = 0.05m;
public Task TrackAdd(ulong userId, long amount, TxData? txData) public Task TrackAdd(ulong userId, long amount, TxData? txData)
{ {
if (txData is null) if (txData is null)
@ -275,6 +304,19 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
.Where(x => x.UserId == userId && x.Game == game) .Where(x => x.UserId == userId && x.Game == game)
.ToListAsync(); .ToListAsync();
} }
public decimal GetHouseEdge(GamblingGame game)
=> game switch
{
GamblingGame.Betflip => 0.025m,
GamblingGame.Betroll => 0.04m,
GamblingGame.Betdraw => 0.04m,
GamblingGame.Slots => 0.034m,
GamblingGame.Blackjack => 0.02m,
GamblingGame.Lula => 0.025m,
GamblingGame.Race => 0.06m,
_ => 0
};
} }
public sealed class UserBetStats public sealed class UserBetStats
@ -306,3 +348,9 @@ public enum GamblingGame
Race = 6, Race = 6,
AnimalRace = 6 AnimalRace = 6
} }
public sealed class Rakeback
{
public ulong UserId { get; set; }
public decimal Amount { get; set; }
}

View file

@ -27,5 +27,22 @@ public sealed class ImagesConfig : ConfigServiceBase<ImageUrls>
c.Version = 5; c.Version = 5;
}); });
} }
if (data.Version < 6)
{
ModifyConfig(c =>
{
if (c.Slots.Emojis?.Length == 6)
{
c.Slots.Emojis =
[
new("https://cdn.nadeko.bot/slots/15.png"),
..c.Slots.Emojis
];
}
c.Version = 6;
});
}
} }
} }

View file

@ -1454,3 +1454,6 @@ translateflags:
- trfl - trfl
- fltr - fltr
- transflags - transflags
rakeback:
- rakeback
- rb

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 11 version: 12
# Currency settings # Currency settings
currency: currency:
# What is the emoji/character which represents the currency # What is the emoji/character which represents the currency
@ -28,7 +28,7 @@ betRoll:
multiplyBy: 10 multiplyBy: 10
- whenAbove: 90 - whenAbove: 90
multiplyBy: 4 multiplyBy: 4
- whenAbove: 66 - whenAbove: 65
multiplyBy: 2 multiplyBy: 2
# Automatic currency generation settings. # Automatic currency generation settings.
generation: generation:
@ -85,7 +85,7 @@ luckyLadder:
- 2.4 - 2.4
- 1.7 - 1.7
- 1.5 - 1.5
- 1.2 - 1.1
- 0.5 - 0.5
- 0.3 - 0.3
- 0.2 - 0.2

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 5 version: 6
coins: coins:
heads: heads:
- https://cdn.nadeko.bot/coins/heads3.png - https://cdn.nadeko.bot/coins/heads3.png
@ -22,15 +22,13 @@ dice:
- https://cdn.nadeko.bot/other/dice/9.png - https://cdn.nadeko.bot/other/dice/9.png
xp: xp:
bg: https://cdn.nadeko.bot/other/xp/bg_k.png bg: https://cdn.nadeko.bot/other/xp/bg_k.png
rip:
bg: https://cdn.nadeko.bot/other/rip/rip.png
overlay: https://cdn.nadeko.bot/other/rip/overlay.png
slots: slots:
emojis: emojis:
- https://cdn.nadeko.bot/slots/0.png - https://cdn.nadeko.bot/slots/10.png
- https://cdn.nadeko.bot/slots/1.png - https://cdn.nadeko.bot/slots/11.png
- https://cdn.nadeko.bot/slots/2.png - https://cdn.nadeko.bot/slots/12.png
- https://cdn.nadeko.bot/slots/3.png - https://cdn.nadeko.bot/slots/13.png
- https://cdn.nadeko.bot/slots/4.png - https://cdn.nadeko.bot/slots/14.png
- https://cdn.nadeko.bot/slots/5.png - https://cdn.nadeko.bot/slots/15.png
- https://cdn.nadeko.bot/slots/16.png
bg: https://cdn.nadeko.bot/slots/slots_bg.png bg: https://cdn.nadeko.bot/slots/slots_bg.png

View file

@ -4662,3 +4662,11 @@ betstats:
desc: 'The game to show betstats for. Omit to show betstats for all games combined' desc: 'The game to show betstats for. Omit to show betstats for all games combined'
- game: - game:
desc: 'The game to show betstats for. Omit to show betstats for all games combined' desc: 'The game to show betstats for. Omit to show betstats for all games combined'
rakeback:
desc: |-
Try to claim any rakeback that you have avaialable.
Rakeback is accumulated by betting (not by winning or losing).
Default rakeback is 0.05 * house edge
House edge is defined per game
params:
- {}

View file

@ -1114,5 +1114,8 @@
"invalid_img_size": "Image must to be {0}x{1} pixels.", "invalid_img_size": "Image must to be {0}x{1} pixels.",
"no_attach_found": "No attachment found. Please send the image along with this command.", "no_attach_found": "No attachment found. Please send the image along with this command.",
"trfl_enabled": "Flag translation enabled on this channel. Reacting to a message with a flag will translate it to that language.", "trfl_enabled": "Flag translation enabled on this channel. Reacting to a message with a flag will translate it to that language.",
"trfl_disabled": "Flag translation disabled." "trfl_disabled": "Flag translation disabled.",
"rakeback_claimed": "You've claimed {0} as rakeback!",
"rakeback_none": "You don't have any rakeback to claim yet.",
"rakeback_available": "You have {0} rakeback available. Click the button to claim."
} }