Skip to content

Gambling — The House Always Wins

The four casino games offered at SpaceDock facilities: Cosmic Slots, Nebula Dice, Sector Sweep lottery, and Stellar Blackjack. Each is a deliberate, high-house-edge credit sink — players who chase the jackpot feed the economy's net drain (see lifecycle.md). Every game settles atomically against the player's wallet under a row lock, so no concurrent request can double-spend a bet or claim an unsettled payout.

Source: services/gameserver/src/api/routes/gambling.py.

A player bets credits, the server resolves the game with a server-side RNG (never trusting client-sent outcomes), deducts the stake, and credits any winnings — all inside one with_for_update() transaction on the player row.


1. Venue gating

Every game requires the player to be docked (Player.is_docked). The route docstring describes gambling as offered "at SpaceDock facilities," and the station model carries a dedicated Station.is_spacedock flag (services/gameserver/src/models/station.py — bang's Port.isSpaceDock sentinel, mirrored gameserver-side).

✅ Shipped — every game is gated on the docked station being a SpaceDock: each handler calls a _require_spacedock guard that returns 403 unless the docked Station.is_spacedock flag is set. Gambling is offered only at SpaceDock facilities, so the docstring's promise holds; "which stations offer gambling" is every SpaceDock a player can dock at.

Source: services/gameserver/src/api/routes/gambling.py (each handler's _require_spacedock guard after the dock check), services/gameserver/src/models/station.py:is_spacedock.

Status: ✅ Shipped — gambling is gated on the docked station being a SpaceDock.


2. Cosmic Slots

A three-reel slot machine. Each reel is drawn independently from a weighted symbol pool; the symbols are 🌍 planet, ⭐ star, 🚀 ship, 💳 credits, 🕳️ black hole, and 💎 jackpot.

Source: services/gameserver/src/api/routes/gambling.py (spin_slots, weighted_random_symbol, SYMBOL_WEIGHTS, SLOT_PAYOUTS).

2.1 Symbol weights

Reel draws are weighted (weights sum to 100, so each weight is a direct percent probability per reel):

Symbol Weight Per-reel probability
🌍 planet 25 25%
⭐ star 25 25%
🚀 ship 20 20%
💳 credits 15 15%
🕳️ black hole 10 10%
💎 jackpot 5 5%

2.2 Payouts

Three of a kind pays the bet times the symbol's multiplier (win_amount = bet × multiplier):

Three of a kind Multiplier
💎💎💎 jackpot 50×
🚀🚀🚀 ship 10×
⭐⭐⭐ star
🌍🌍🌍 planet
💳💳💳 credits
🕳️🕳️🕳️ black hole 0× (loses the bet)

Two adjacent matching symbols (reels 1–2 or reels 2–3 equal, but not all three) return half the bet (bet // 2). Any other outcome loses the bet. A jackpot result (💎💎💎) sets the response jackpot flag.

2.3 Return to player

With these weights and payouts the machine's return to player is RTP ≈ 45.6% — a house edge of ≈ 54.4%. The three-of-a-kind tier contributes ≈ 30.0% RTP; the two-adjacent-match consolation (probability ≈ 31.25%, paying only 0.5×) adds ≈ 15.6%. The black-hole symbol exists purely to deepen the edge: three of them pays nothing.

Status: ✅ Shipped — live and concurrency-hardened: the bet deduction and payout are atomic under a with_for_update() player-row lock with populate_existing() (row-locked settle, commit 9a91895).


3. Nebula Dice

Two six-sided dice. The player bets on the total: high (8–12), low (2–6), or an exact total (2–12). Two outcomes override the bet type.

Source: services/gameserver/src/api/routes/gambling.py (roll_dice).

3.1 Special outcomes

Outcome Trigger Payout
Supernova double sixes (6 + 6) 35× the bet, regardless of bet type
The Void total of 7 the house always wins — 0×

The Void claims the natural mode of two dice (total 7 is the single most likely roll), which is why neither high nor low covers it.

3.2 Bet types

Bet type Wins when Payout
high total is 8–12 (and not 7, not double-six)
low total is 2–6
exact N total equals the chosen N per-target multiplier (below)

3.3 Exact-total payouts

An exact bet pays a multiplier scaled to the rarity of each total:

Total N Multiplier
2 35×
3 17×
4 11×
5
6
7
8
9
10 11×
11 17×
12 35×

An exact bet on 7 nominally pays 5×, but a rolled 7 is The Void (house wins), so an exact-7 bet can only ever pay out via a double-six Supernova (which pays 35×, not 5×). An unrecognised exact target defaults to 5×.

Status: ✅ Shipped — live and concurrency-hardened: row-locked atomic settle (commit 9a91895).


4. Sector Sweep (lottery)

A pick-4 lottery. The player chooses 4 unique sector numbers from 1–12; the server draws 4 unique winning numbers from the same pool (random.sample(range(1,13), 4)). The payout scales with how many of the player's numbers match the draw.

Source: services/gameserver/src/api/routes/gambling.py (buy_lottery_ticket, LOTTERY_PAYOUTS).

Ticket cost is bounded 100–5,000 credits per ticket. All four chosen numbers must be unique and within 1–12, or the request is rejected.

4.1 Match-tier payouts

Matches Multiplier Result
4 1000× Jackpot
3 50×
2
1 returns the bet
0 loses the bet

Matching all 4 (a 4-of-12 sweep) is the jackpot and sets the response jackpot flag; matching exactly 1 returns the stake (break-even).

Status: ✅ Shipped — live and concurrency-hardened: row-locked atomic settle (commit 9a91895).


5. Stellar Blackjack

A single-hand blackjack table against the dealer. The flow is two endpoints: /blackjack/deal opens a hand, /blackjack/action resolves each hit / stand / double until the hand ends.

Source: services/gameserver/src/api/routes/gambling.py (blackjack_deal, blackjack_action, reconstruct_blackjack_hands).

5.1 Bet bounds

A blackjack stake is bounded 10–10,000 credits (the same bound applies to slots and dice; lottery tickets use the 100–5,000 range). The stake is deducted at deal time.

5.2 Rules

  • Standard card values: face cards 10, ace 11 or 1 (soft-counted down automatically when the hand would bust).
  • The deck is a single 52-card deck, shuffled with a server-chosen seed at deal time.
  • The dealer shows one card and holds one card hidden until the player stands or busts.
  • Natural blackjack (player's first two cards total 21) pays 3:2 (win_amount = bet × 2.5); if the dealer also has a natural it is a push (bet returned).
  • The dealer hits on 16 or less and stands on 17 or more (stands on all 17s, including soft).
  • Double down is allowed only on the first two cards: the stake doubles, the player takes exactly one card, and the hand stands. A player who busts after doubling always loses — a busted hand never out-ranks the dealer.
  • A non-blackjack win pays (even money); a push returns the stake; a bust or a loss to the dealer forfeits the stake.

5.3 Server-authoritative anti-faucet design

Blackjack is fully server-authoritative to close credit-fabrication exploits. The server stores the authoritative game state (deck seed, stake, player-card count) on the player record at deal time and rebuilds both hands deterministically from the seed on every action — client-sent cards, seed, and bet are ignored. Consequences:

  • A payout cannot be claimed without a real, un-settled deal: /blackjack/action with no stored active hand is rejected ("deal first").
  • A settled hand cannot be replayed for a second payout — the stored game is cleared the moment the hand ends.
  • A double-down deducts the stored stake, never a client-supplied amount, so the bet cannot be inflated mid-hand.
  • The player row is locked (with_for_update()) across the deal/action so two concurrent requests cannot run two live hands or double-spend.

Status: ✅ Shipped — live, server-authoritative, and concurrency-hardened: anti-faucet server-side hand reconstruction landed in commit b0b988d; the row-locked atomic settle pattern matches the other three games.


6. Cross-references

  • lifecycle.md — the full credit faucet/sink loop; gambling is catalogued there as a net-house-edge sink.
  • trading.md — the legitimate way to grow credits (gambling is the opposite).
  • docking-slips.md — docking at stations, the precondition for gambling.