Skip to content
Jan Kluka edited this page Jul 31, 2026 · 6 revisions

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>.jar into plugins/X-Prison/addons/ and restart. Requires the Currency module to be enabled. Requires X-Prison 2026.3.2.0 or newer.


Files

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.


How the house edge works

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.

The money-printer guard

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.


Games

Slots

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 it

How 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.

Roulette

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.0

Add or remove bets freely — the menu and the RTP both follow whatever you define.

Dice

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: 50

The one-click chances offered in the menu are bet.chance-buttons in the same file, and are yours to change.

Crash

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.04

The 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.

Wagering and limits

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: 1000000000000000

Every key here can be overridden in a game's own file.

The payout ceiling

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.

Wager buttons

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 give10K, 25M, 10S:

input:
  enabled: true
  cancel-word: "cancel"
  timeout-seconds: 30

Daily loss limit

limits:
  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.


Opening and closing the casino

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.

Tables a player cannot use

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.


Nothing shown to a player is hardcoded

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 secret

A 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.

Custom heads and models

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: 1001

Heads 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.


Sounds

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.

Per-game cues

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.3

Out 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

animation:
  enabled: true
  duration-ticks: 40
  frame-interval-ticks: 2
  skippable: true
  remember-skip-choice: true

The 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.


Commands

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.


Placeholders

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 storage

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 lists

Rounds 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.


Broadcasts

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: 1000000000000000

Set 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.


Performance

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.


Adding your own game

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.

XPrison Logo

General

Modules

Default Configs

Enchant Configs — Passive

Enchant Configs — Currency Rewards

Enchant Configs — Key & Item Rewards

Enchant Configs — Area of Effect

Enchant Configs — Multipliers

Enchant Configs — Templates

Enchant Configs — Addons

Addons

Support

For Developers

Others

Clone this wiki locally