mirror of
https://github.com/horizon-rust/horizon-mods.git
synced 2026-11-07 11:05:58 -10:00
SMP 0.28.0: the casino's daily win cap (10,000 net a day, configurable)
A player whose net casino winnings today reach casino.daily_win_cap (10,000, same as the loss cap) is refused new bets until 00:00 UTC, with a message saying why. The round that crosses the cap is paid in full; a blackjack double inside a hand already dealt is still allowed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
5ba617927f
commit
31d77513fe
7 files changed
+61
-6
No files matched your search
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "horizon-mod-smp"
|
||||
version = "0.27.0"
|
||||
version = "0.28.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "horizon-mod-smp"
|
||||
version = "0.27.0"
|
||||
version = "0.28.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
@@ -202,7 +202,8 @@ jackpot:
|
||||
first two cards, no split; about 1% house edge with basic strategy.
|
||||
|
||||
Every draw is made by the server (ChaCha20 keyed from the system) before the animation starts. Bets go from `min_bet`
|
||||
10 to `max_bet` 1000; a player may lose at most `daily_loss_cap` (10,000) a day (UTC, all games together), and
|
||||
10 to `max_bet` 1000; a player may lose at most `daily_loss_cap` (10,000) a day (UTC, all games together) and win at most
|
||||
`daily_win_cap` (10,000) net (once reached, no new bet until 00:00 UTC; the round that crosses it is paid in full), and
|
||||
`cooldown_ms` (2000) passes between the end of a round and their next bet. A bet is held like a coinflip stake;
|
||||
closing the menu or leaving never loses a round: a spin or a pull ends and is paid, a blackjack hand stands, a crash
|
||||
bet is cashed out when its player leaves in flight (or given back before take-off). Wins are paid from the casino's
|
||||
|
||||
@@ -43,7 +43,7 @@ use std::{path::PathBuf, sync::Mutex};
|
||||
/// The mod's id, name and version.
|
||||
pub const ID: &str = "smp";
|
||||
pub const NAME: &str = "SMP";
|
||||
pub const VERSION: &str = "0.27.0";
|
||||
pub const VERSION: &str = "0.28.0";
|
||||
|
||||
/// `config.json` in `world/horizon-smp/`, written with its defaults the
|
||||
/// first time, and again with the keys a newer version adds (values kept).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[mod]
|
||||
id = "smp"
|
||||
version = "0.27.0"
|
||||
version = "0.28.0"
|
||||
priority = 100
|
||||
|
||||
# The commit of horizon it was tested on: it needs horizon#6106 (merged at 975c07d): native_inventory,
|
||||
|
||||
@@ -48,6 +48,9 @@ pub struct Config {
|
||||
/// What one player may lose in a day (UTC), all games together: bets
|
||||
/// staked minus what they paid back.
|
||||
pub daily_loss_cap: i64,
|
||||
/// What one player may win in a day (UTC), all games together: paid back
|
||||
/// minus bets staked. Once reached, no new bet until the day ends.
|
||||
pub daily_win_cap: i64,
|
||||
/// Between the end of a player's round and their next bet.
|
||||
pub cooldown_ms: i64,
|
||||
/// What the pot keeps for paying wins; above it, the Treasury's.
|
||||
@@ -69,6 +72,7 @@ impl Default for Config {
|
||||
min_bet: 10,
|
||||
max_bet: 1000,
|
||||
daily_loss_cap: 10_000,
|
||||
daily_win_cap: 10_000,
|
||||
cooldown_ms: 2000,
|
||||
float: 20_000,
|
||||
announce_profit: 5000,
|
||||
@@ -358,6 +362,10 @@ impl Day {
|
||||
pub fn lost(&self) -> i64 {
|
||||
(self.wagered - self.paid).max(0)
|
||||
}
|
||||
|
||||
pub fn won(&self) -> i64 {
|
||||
(self.paid - self.wagered).max(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A win of the day.
|
||||
@@ -510,6 +518,19 @@ impl Casino {
|
||||
(self.config.daily_loss_cap - self.day(uuid, now).lost()).max(0)
|
||||
}
|
||||
|
||||
/// Err once `uuid`'s winnings today reached the daily win cap.
|
||||
fn win_cap(&self, economy: &Economy, uuid: u128, now: i64) -> Result<(), String> {
|
||||
let won = self.day(uuid, now).won();
|
||||
if won >= self.config.daily_win_cap {
|
||||
return Err(format!(
|
||||
"Daily win limit: you have won {} today, the most a day ({}). The casino opens again for you at 00:00 UTC.",
|
||||
economy.money(won),
|
||||
economy.money(self.config.daily_win_cap)
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_for_play(&self) -> Result<(), String> {
|
||||
if !self.gambling || !self.config.enabled {
|
||||
return Err("The casino is closed on this server.".into());
|
||||
@@ -546,6 +567,7 @@ impl Casino {
|
||||
wait as f64 / 1000.0
|
||||
));
|
||||
}
|
||||
self.win_cap(economy, uuid, now)?;
|
||||
let left = self.loss_left(uuid, now);
|
||||
if amount > left {
|
||||
return Err(format!(
|
||||
@@ -792,10 +814,11 @@ impl Casino {
|
||||
let c = &self.config;
|
||||
let mut lines = vec![
|
||||
format!(
|
||||
"Casino: bets {} to {}; you may lose at most {} a day; {} s between rounds.",
|
||||
"Casino: bets {} to {}; you may lose at most {} and win at most {} a day; {} s between rounds.",
|
||||
economy.money(c.min_bet),
|
||||
economy.money(c.max_bet),
|
||||
economy.money(c.daily_loss_cap),
|
||||
economy.money(c.daily_win_cap),
|
||||
c.cooldown_ms as f64 / 1000.0
|
||||
),
|
||||
"Every draw is made by the server (ChaCha20 seeded by the system), before the animation starts.".into(),
|
||||
@@ -1699,6 +1722,7 @@ pub(crate) mod tests {
|
||||
let (mut e, mut c, dir) = setup("cost", 1_000_000);
|
||||
c.config.cooldown_ms = 0;
|
||||
c.config.daily_loss_cap = i64::MAX / 4;
|
||||
c.config.daily_win_cap = i64::MAX / 4;
|
||||
let players: Vec<u128> = (100..120).collect();
|
||||
for (i, u) in players.iter().enumerate() {
|
||||
e.seen(*u, &format!("P{i}"));
|
||||
@@ -1801,6 +1825,35 @@ pub(crate) mod tests {
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_daily_win_cap_stops_new_bets() {
|
||||
let (mut e, mut c, dir) = setup("wincap", 50_000);
|
||||
c.config.daily_win_cap = 1000;
|
||||
c.config.cooldown_ms = 0;
|
||||
// Won 700 net: still under the cap.
|
||||
let s = c.bet(&mut e, A, "Alex", Game::Roulette, 700, 0).unwrap();
|
||||
c.settle(&mut e, &s, 1400, "won", 0);
|
||||
assert_eq!(c.day(A, 0).won(), 700);
|
||||
// Won 700 more: 1400 net, past the cap; the round that crossed it is paid in full.
|
||||
let s = c.bet(&mut e, A, "Alex", Game::Roulette, 700, 0).unwrap();
|
||||
c.settle(&mut e, &s, 1400, "won", 0);
|
||||
assert_eq!(e.balance(A), 10_000 + 1400);
|
||||
let err = c.bet(&mut e, A, "Alex", Game::Slots, 10, 0).unwrap_err();
|
||||
assert!(
|
||||
err.contains("Daily win limit") && err.contains("1,400"),
|
||||
"{err}"
|
||||
);
|
||||
// Nothing was held for the refused bet.
|
||||
assert_eq!((e.balance(A), e.held(A)), (11_400, 0));
|
||||
// Another player isn't affected; a new day opens it again.
|
||||
c.bet(&mut e, B, "Steve", Game::Roulette, 10, 0).unwrap();
|
||||
assert!(
|
||||
c.bet(&mut e, A, "Alex", Game::Roulette, 10, DAY_MS + 1)
|
||||
.is_ok()
|
||||
);
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wins_come_from_the_pot_and_are_capped_by_it() {
|
||||
let (mut e, mut c, dir) = setup("pot", 1000);
|
||||
|
||||
@@ -504,6 +504,7 @@ mod tests {
|
||||
fn many_spins_keep_about_the_house_edge() {
|
||||
let (mut e, mut c, dir) = setup("edge", 1_000_000_000);
|
||||
c.config.daily_loss_cap = i64::MAX / 4;
|
||||
c.config.daily_win_cap = i64::MAX / 4;
|
||||
c.config.cooldown_ms = 0;
|
||||
let mut rng = Rng::from_seed([3; 32]);
|
||||
let mut now = 0;
|
||||
|
||||
Reference in new issue
Block a user