-
Notifications
You must be signed in to change notification settings - Fork 58
Casino
Four solo games played against the house — Slots, Roulette, Dice and Crash.
Every wager the house wins is destroyed. The Casino is a currency sink: it exists to take currency out of an inflated economy, never to put currency into it. That is the opposite of the Coinflip addon, which is player-versus-player and simply moves currency between two players.
Addon — drop
X-Prison-Casino-<version>.jarintoplugins/X-Prison/addons/and restart. Requires the Currency module to be enabled. Requires X-Prison2026.3.2.0or newer.
Everything about a single game — its odds, its limits and its own menus — lives in that game's own file. Delete the file and that table is gone from the casino, with one line in the console saying so and nothing else to change. Copying a file does not create a new table: a new game is a developer job, described under Adding your own game.
| File | Purpose |
|---|---|
Casino/casino.yml |
Settings shared by the whole casino: currencies, defaults, limits, animation, sounds |
Casino/games/slots.yml |
Slots — odds, limits, bet menu, result menu |
Casino/games/roulette.yml |
Roulette — pockets, bets, menus |
Casino/games/dice.yml |
Dice — chance range, menus |
Casino/games/crash.yml |
Crash — multiplier curve, bet menu, live menu |
Casino/casino-gui.yml |
Menus not owned by a single game (the hub) |
Casino/casino-messages.yml |
Every message, raw MiniMessage |
Casino/state.yml |
Which tables are closed for maintenance (managed by the plugin) |
All of it reloads with /casino reload. state.yml is written by the plugin — do not edit it by
hand.
Your files are never rewritten by an update, so your comments and your edits survive. A message that
a later version adds and your casino-messages.yml therefore has no line for falls back to the
wording bundled in the jar, and the console names it once at startup so you can add and reword it
when you want to.
Anything a game leaves unset falls back to the defaults block in casino.yml, so you can set a
wager minimum or a house edge once and have every game inherit it.
Each game works out a fair payout multiplier from its own odds, then scales it down by
house-edge. An edge of 0.05 means players get back 95% of what perfectly fair odds would pay.
| Game | Fair multiplier |
|---|---|
| Slots | the payout configured for the symbol combination that landed |
| Roulette |
pockets ÷ winning-pockets for the bet that was backed |
| Dice | 100 ÷ win chance |
| Crash | baked into the crash-point distribution itself |
The resulting return to player (RTP) is printed in the console when each game is enabled, and
listed by /casino rtp.
A game configured with an RTP of 100% or more pays out more than it takes in — it would create
currency instead of destroying it. The Casino refuses to enable such a game and prints an error
naming it, rather than quietly running it. Lower its payouts or raise its house-edge and reload.
This is why house-edge: 0 is not a valid setup for Dice or Crash: with no edge they return exactly
100%, and they will not start.
Every reel is drawn from the same weighted symbol pool. A symbol's chance is its weight divided by the total of all weights.
# games/slots.yml
reels: 3
symbols:
CHERRY: 40
SEVEN: 1
payouts:
all-matching: # every reel showing the same symbol
CHERRY: 2.0
SEVEN: 15000.0
two-matching: 1.2 # exactly two reels match; 0 disables itHow each symbol looks is configured further down the same file, under result.symbols.
The RTP is computed by enumerating every possible combination exactly. If your table has more combinations than can be checked (symbols raised to the power of reels), the game refuses to load and asks you to reduce one of the two.
One pocket is drawn from 0 to pockets - 1. Each bet declares how many of those pockets win it.
# games/roulette.yml
pockets: 37
default-bet: "red"
bets:
red:
winning-pockets: 18 # an 18-in-37 chance
payout: 2.0555 # fair, because 37 ÷ 18 = 2.0555
green:
winning-pockets: 1
payout: 37.0Add or remove bets freely — the menu and the RTP both follow whatever you define.
The player picks a win chance; the payout scales inversely with it. A 5% shot pays roughly twenty times the stake, a 90% shot barely more than the stake.
# games/dice.yml
min-chance: 1
max-chance: 95 # must stay below 100, or the game could not lose
default-chance: 50The one-click chances offered in the menu are bet.chance-buttons in the same file, and are yours
to change.
A multiplier climbs from 1.00x. Cash out before it crashes or lose the wager.
# games/crash.yml
max-multiplier: 100.0
tick-interval: 4 # ticks between steps — lower is faster and tenser
growth-per-tick: 0.04The chance of reaching any multiplier is (1 − house-edge) ÷ x, so at a 5% edge a player cashing
out at 2.00x wins 47.5% of the time and one at 1.50x wins 63%. The edge is real but only
shows over volume, which is worth knowing before you judge it by a handful of rounds.
Crash is the only game that settles while its menu is open, which makes it the only one that can be interrupted. Two safeguards cover that:
- Closing the menu cashes out at the multiplier that was on screen. Logging out, dying or walking through a portal can never strand a wager. While a round is live the menu deliberately offers no Back button, so cashing out is always something a player chose rather than something that happened to them on the way out.
- A server crash refunds the wager. The round is recorded in the database before its menu is built, so even a disconnect in the instant between paying and seeing the menu is covered. Anything left open is refunded on the next startup, with a line in the console saying how many.
defaults:
currencies: [ "Tokens" ] # which currencies may be wagered
min-wager: 1000000000
max-wager: 0 # 0 = no upper limit; set per game instead
house-edge: 0.05
cooldown-seconds: 0
permission: "" # extra permission on top of xprison.casino.game.<id>
max-payout: 10000000000000000 # the most any single round may pay. 0 = no ceiling
big-win-threshold: 1000000000000000Every key here can be overridden in a game's own file.
max-payout is the safety valve for whales, and it is the one limit worth understanding.
A wager is refused up front if its best possible payout would pass the ceiling — never quietly short-paid on a win. A player allowed to place a wager is always paid in full.
Because each game has a different top multiplier, one ceiling produces a different maximum wager per game. That is the point:
| Game | Top multiplier | Max wager under a 10¹⁶ ceiling |
|---|---|---|
| Slots | 14,250× (three sevens) | 700B |
| Roulette | 35.15× (green) | 280T |
| Dice | 95× (a 1% chance) | 100T |
| Crash | 100× (max-multiplier) |
100T |
The shipped defaults set both — max-payout globally and a matching max-wager per game — so the
two never disagree. If you raise one, raise the other.
The buttons in a bet menu are entirely yours. Each entry under bet.wager-buttons sets the wager to
its own amount; add, remove or rename as many as you like, in any slots. Nothing assumes a
particular number of them.
bet:
wager-buttons:
tier1:
slot: 20
amount: 1000000000
material: IRON_NUGGET
name: "<white><bold>%amount%"%amount% renders through that currency's own formatter, so a button always agrees with how the
currency is written everywhere else on the server.
Players can also type a wager in chat. The custom-wager-button closes the menu and reads the
next thing they type, accepting the same short forms as /currency give — 10K, 25M, 10S:
input:
enabled: true
cancel-word: "cancel"
timeout-seconds: 30limits:
daily-net-loss-cap: 0 # stop a player once they are this far down today; 0 = no cap
bypass-permission: "xprison.casino.bypass.limits"The daily tally is kept in memory and survives a relog, so reconnecting cannot reset a player's limit. It does reset when the server restarts, and at midnight in the server's timezone.
All amounts are exact BigDecimal values end to end — OP-scale balances above 10²⁴ wager and pay
out without losing a single digit.
Tables can be closed without touching a config or restarting.
/casino maintenance all <on|off> close or reopen the whole casino
/casino maintenance <game> <on|off> close or reopen one game
While a game is closed, its slot in the hub is replaced by a configurable maintenance item, and the game is refused at the hub, at the command, and at the wager itself — every route a round could take.
# casino-gui.yml
hub:
maintenance-item:
material: BARRIER
name: "<gradient:#ff9d9d:#c0392b><bold>%game% — CLOSED</bold></gradient>"
lore:
- "<gray>This table is closed for maintenance."The state is stored in state.yml, so a restart in the middle of an incident cannot silently reopen
a table you closed. Staff holding xprison.casino.bypass.maintenance can still play a closed table,
which is how you test a fix before reopening it.
A game a player lacks xprison.casino.game.<id> for (or the game's own extra permission) is drawn
as a locked item rather than as a table they can click and be refused at:
# casino-gui.yml
hub:
locked-item:
material: IRON_BARS
name: "<dark_gray><bold>%game%</bold>"
lore:
- "<gray>You do not have access to this table."
- "<dark_gray>%permission%"%permission% is the node they are missing, which makes this a natural place to advertise a rank
perk. Delete the whole locked-item section and the game is shown normally, with the refusal
message explaining it instead.
Every piece of text a player can see comes from a config file. Menus live in each game's own file
(and casino-gui.yml for the hub), messages in casino-messages.yml, and the small fragments the
plugin has to substitute live under display in casino.yml:
display:
unlimited: "<gray>Unlimited" # shown wherever a wager has no upper limit
selected: "<green><bold>✔" # marks the bet or chance a player has picked
not-selected: ""
hidden: "<dark_gray>???" # stands in for the Crash multiplier while it is secretA bet menu's title is live: it takes any placeholder that menu takes — %wager%, %balance%,
%currency% — and is re-sent to the player as they change their wager, so the amount they are about
to stake is readable without hunting for the item that holds it. The shipped titles use it:
# games/dice.yml
bet:
title: "<dark_gray>▎ <gradient:#7fe7ff:#2f86c4><bold>DICE</bold></gradient> <dark_gray>▏ <gray>%wager%"Retitling an open window needs Minecraft 1.20 or newer. On anything older the menu simply keeps the title it opened with, which already carries the opening wager.
Each game also takes a display-name, used everywhere the game is named — menus, messages,
broadcasts and /casino stats:
# games/slots.yml
display-name: "<gradient:#f5d76e:#e08f2c>Slots</gradient>"Roulette bets take one too, so red can read as anything you like.
All of it is raw MiniMessage, so gradients, hover and click tags work throughout.
Any item in any menu accepts a custom head or a resource-pack model:
material: BARRIER # fallback if the texture cannot be built
head-texture: "eyJ0ZXh0dXJlcyI6..." # base64 from minecraft-heads.com and friends
custom_model_data: 1001Heads are built once at startup and cached, never per click. They are resolved from the offline
base64 value, so the server never contacts Mojang for them — building a head any other way makes
Paper attempt a lookup on the main thread and hang until it times out. If a texture is malformed the
item falls back to its material and says so once in the console.
Every cue is configurable, and any of them can be silenced with enabled: false or sound: none.
sounds:
menu-open: { enabled: true, sound: BLOCK_BARREL_OPEN, volume: 0.5, pitch: 1.4 }
click: { enabled: true, sound: UI_BUTTON_CLICK, volume: 0.5, pitch: 1.6 }
wager-change: { enabled: true, sound: BLOCK_NOTE_BLOCK_HAT, volume: 0.6, pitch: 1.8 }
select: { enabled: true, sound: BLOCK_NOTE_BLOCK_PLING, volume: 0.6, pitch: 1.5 }
denied: { enabled: true, sound: BLOCK_NOTE_BLOCK_BASS, volume: 0.5, pitch: 0.7 }
round-start: { enabled: true, sound: BLOCK_LEVER_CLICK, volume: 0.9, pitch: 0.8 }
reel-tick: { enabled: true, sound: BLOCK_NOTE_BLOCK_HAT, volume: 0.3, pitch: 1.2 }
win: { enabled: true, sound: ENTITY_PLAYER_LEVELUP, volume: 0.8, pitch: 1.4 }
big-win: { enabled: true, sound: UI_TOAST_CHALLENGE_COMPLETE, volume: 1.0, pitch: 1.0 }
loss: { enabled: true, sound: ENTITY_VILLAGER_NO, volume: 0.7, pitch: 0.9 }
push: { enabled: true, sound: BLOCK_NOTE_BLOCK_BELL, volume: 0.6, pitch: 1.0 }
crash-tick: { enabled: true, sound: BLOCK_NOTE_BLOCK_HAT, volume: 0.4, pitch: 1.0 }
crash-cashout:{ enabled: true, sound: ENTITY_PLAYER_LEVELUP, volume: 0.9, pitch: 1.6 }
crash-bust: { enabled: true, sound: ENTITY_GENERIC_EXPLODE, volume: 0.7, pitch: 1.3 }Two cues climb in pitch rather than repeating flatly: reel-tick rises as the reels slow down, and
crash-tick rises with the multiplier. denied is what a click that could not do anything sounds
like — already at the wager cap, or only one currency to switch between.
big-win replaces win whenever a payout reaches that game's big-win-threshold.
Sound names accept both spellings — the registry key block.barrel.open (optionally namespaced) and
the legacy enum name BLOCK_BARREL_OPEN.
A game file can declare its own sounds: block, overriding the casino-wide set cue by cue. Name
only what should differ; everything else keeps the shared sound. This is what gives each table its
own character:
# games/roulette.yml
sounds:
round-start:
sound: BLOCK_NOTE_BLOCK_CHIME # the wheel set spinning
volume: 0.8
pitch: 0.8
reel-tick:
sound: BLOCK_STONE_BUTTON_CLICK_ON # the ball rattling across the frets
volume: 0.45
pitch: 1.3Out of the box each game already sounds distinct:
| Game | Starting the round | While it plays out |
|---|---|---|
| Slots | a mechanical lever | reels clacking round |
| Roulette | the wheel set spinning | the ball across the frets |
| Dice | dice leaving the hand | dice tumbling |
| Crash | a rocket leaving the pad | a climbing electronic tone |
Any cue can be overridden this way, not just the animation ones — a game can have its own win, loss or denied sound just as easily.
animation:
enabled: true
duration-ticks: 40
frame-interval-ticks: 2
skippable: true
remember-skip-choice: trueThe round is already settled before its menu opens — the animation is pure display. Skipping it, closing the menu, or logging out mid-spin cannot change or lose the result.
With remember-skip-choice: true, clicking the skip button also skips future animations for that
player. /casino skip toggles it back on. The choice is stored, so it survives a relog and a
restart — a player who turns animations off stays turned off until they say otherwise.
| Command | Permission | What it does |
|---|---|---|
/casino |
xprison.casino.use |
Opens the casino hub |
/casino help |
xprison.casino.use |
Lists the commands the sender can actually use |
/casino <game> [wager] |
xprison.casino.game.<game> |
Opens that game's bet menu |
/casino open <player> |
xprison.menu.other |
Opens the hub on another player's screen (NPC support) |
/casino stats [player] [currency] |
xprison.casino.use |
An overall line plus one line per game |
/casino stats <someone else> |
xprison.casino.stats.others |
Reading another player's record |
/casino top [currency] |
xprison.casino.use |
Players who are up on the house |
/casino skip |
xprison.casino.use |
Toggles animation skipping |
/casino rtp |
xprison.casino.admin |
Prints each game's configured return to player |
/casino maintenance <all|game> <on|off> |
xprison.casino.admin |
Closes or reopens the casino, or one table |
/casino reload |
xprison.casino.reload |
Reloads every config file |
Two permissions are worth granting deliberately: xprison.casino.bypass.maintenance lets staff play
a closed table, and xprison.casino.bypass.limits lifts the daily loss cap.
The help text itself is help_header, help_player and help_admin in casino-messages.yml —
both lists are yours to reword, extend or empty, and the admin lines are only ever sent to staff.
The command name and its aliases are set in casino.yml under command.
Requires PlaceholderAPI.
| Placeholder | Value |
|---|---|
%xcasino_wins% |
Rounds won |
%xcasino_losses% |
Rounds lost |
%xcasino_rounds% |
Rounds played |
%xcasino_wagered% |
Total wagered |
%xcasino_net% |
Net result against the house |
%xcasino_biggest_win% |
Largest single payout |
%xcasino_net_<currency>% |
Net result in a specific currency |
%xcasino_wagered_<currency>% |
Total wagered in a specific currency |
%xcasino_biggest_win_<currency>% |
Largest payout in a specific currency |
%xcasino_rtp_<game>% |
A game's configured return to player |
%xcasino_min_wager_<game>% |
A game's minimum wager |
%xcasino_<stat>_game_<game>% |
Any of the stats above, scoped to one game — e.g. %xcasino_net_game_crash%
|
stats:
enabled: true
retention-days: 30 # prune round history older than this on startup; 0 = keep forever
top-limit: 10 # how many players /casino top listsRounds are written to the X-Prison database as an audit trail, and running totals are kept per player,
per currency and per game — which is what lets /casino stats show an overall line followed by
one line for each game, and what the _game_ placeholders read from. Amounts are stored as text
rather than numbers, because SQLite has no exact decimal type and would round OP-scale wagers.
A payout at or above a game's big-win-threshold is announced to the whole server and plays the
big-win cue instead of the ordinary win one. It sits alongside that game's other limits, so a
threshold that suits Slots does not have to suit Crash:
# casino.yml
broadcast:
enabled: true
permission: "" # blank = everyone sees them; set a node to make them opt-in
defaults:
big-win-threshold: 1000000000000000 # the shared default
# games/slots.yml — this game only
big-win-threshold: 1000000000000000Set the threshold to 0 to never announce, or broadcast.enabled: false to silence them all. The message itself is big_win_broadcast in
casino-messages.yml.
The casino is built to survive a full table of players spinning at once.
- A menu's backdrop — its filler and decorations — is drawn once, not per animation frame.
- A spinning menu redraws only its reels; the round's amounts are formatted once, not per frame.
- Custom heads are built at startup and only cloned afterwards.
- Round history is written off the main thread.
If you ever want to spend even less, animation.frame-interval-ticks is the dial — raising it from
2 to 4 halves the work — and animation.enabled: false removes the cost entirely.
The Casino is built around a CasinoGame interface. A new game is one class, one registry line and
one file in games/ — no existing code is edited. Developers can also listen for CasinoWagerEvent
(cancellable, with a mutable amount) and CasinoRoundSettledEvent.
- Currencies
- Ranks
- Prestiges
- Rebirths
- Mines
- AutoSell
- AutoMiner
- Enchants
- Pickaxe Levels
- Pickaxe Skins
- Pickaxe Quality
- Pickaxe Settings
- Gangs
- Multipliers
- Blocks
- Bombs
- History
- Mining Stats
- Nicknames
- Battle Pass
- Quests
- Daily Rewards
- Milestones
- config.yml
- autominer.yml
- autosell.yml
- block-rewards.yml
- enchants.yml
- currencies.yml
- multipliers.yml
- ranks.yml
- prestiges.yml
- pickaxe-levels.yml
- pickaxe-skins.yml
- pickaxe-quality.yml
- gangs.yml
- mines.yml
- bombs.yml
- blocks.yml
- history.yml
- logging.yml
- mining-stats.yml
- rebirths.yml
- battlepass.yml
- quests.yml
- dailyrewards.yml
- milestones.yml
- efficiency.json
- fortune.json
- unbreaking.json
- haste.json
- speed.json
- fly.json
- nightvision.json
- jumpboost.json
- autosell.json
- tokenfinder.json
- gemfinder.json
- salary.json
- charity.json
- blessing.json
- gangvaluefinder.json
- prestigefinder.json
