Discord bot originally built for randomly generating Old West names and now wandering off into card games.
See DISCORD_SETUP.md for instructions on creating and configuring a Discord bot for this app.
Names were scraped from Mithril and Mages.
/wwname [gender] [number]: GeneratesnumberOld West names by choosing a random first name of the givengenderand a random surname. Thegenderargument accepts any string starting withform. If nogenderis given, a random one is chosen. If not provided,numberdefaults to 1. The output is a newline-joined list of names in the form of<gender emoji> first_name last_name.
All gameplay actions have slash commands. As a convenience, join, leave, bet <amount>, hit, and stand also work typed as plain messages in the game's channel (no slash prefix).
Slash commands:
/help— show all available commands./newgame [num_bots](admin) — starts a new game of blackjack.num_bots(0–4, default 0) spawns bot players with Wild West personalities. Bots use AI-powered decisions if an API key is configured, otherwise they fall back to basic strategy./joingame— sit down at the table./leavegame— leave the current game./bet <amount>— place a bet during the betting phase./hit— draw another card on your turn./stand— hold your hand on your turn./wad— privately check your current balance (only visible to you)./stats— view your saloon record (hands played, winnings, etc.; only visible to you)./saloon— show the saloon's name, town, and active tables./changelog— show what's new at the saloon: the last 5 entries inCHANGELOG.md, or the last week's worth, whichever is more./stopgame(admin) — end the current game immediately and return all unresolved bets to players./checkwallet <target>(admin) — check any player's or NPC's wallet balance./setwallet <target> <amount>(admin) — set a player's or NPC's wallet to an exact amount./givechips <target> <amount>(admin) — adjust a player's or NPC's wallet by a delta (negative takes chips away)./npclimits [min] [max](admin) — view or set the NPC autofill min/max per table. Withmin > 0, tables stay populated with NPCs even when no humans are present./npcrelationships <npc>(admin) — list an NPC's relationships (partner, type, strength, and the story behind them)./addnpc [count](admin) — add one or more roster NPCs to the current game (default: 1)./removenpc [name](admin) — remove an NPC from the current game; omit name to remove any NPC./usage [days](admin) — show a summary of LLM token usage (default 7 days, up to 90), broken down by purpose, model, and provider./debug(admin) — dump full internal state for troubleshooting, including live LLM provider health (up/down, last success/failure, whether NPCs are currently on AI or fallback strategy).
/version: Outputs the current git sha.
SaloonBot supports AI-powered bot players with Wild West personalities. Bots use an LLM to make real gameplay decisions (hit/stand/bet) and deliver in-character quips during the game.
Bots are drawn from a pool of 15 archetypes and 4 historical figures, each with a distinct voice and betting temperament. Bot quips appear in sepia-colored embeds to distinguish them from regular game messages.
Archetypes: The Grizzled Prospector, The Drunk Cowboy, The Snake Oil Salesman, The Prim Schoolmarm, The Bounty Hunter, The Frontier Preacher, The Railroad Baron, The Half-Broke Drifter, The Card Sharp, The Saloon Singer, The Greenhorn Deputy, The Apache Tracker, The Patent Medicine Widow, The Retired Outlaw, The Railroad Cook
Historical figures: Doc Holliday, Calamity Jane, Jesse James, Wild Bill Hickok
AI bots remember their nights at the table. While seated, each bot keeps track of what happens around it — bets, busts, wins, and banter — and when it leaves (including deciding on its own to "call it a night"), it condenses the session into a short memory that persists in the database. The next time that NPC sits down, its recent memories feed into its prompts, so a regular NPC can reference past sessions and the people it played with. Memory depth follows SALOON_DETAIL_LEVEL: low disables the feature, medium recalls 1 recent session, high recalls 3.
Roster NPCs have history with each other. A newly created NPC has a 70% chance of arriving with 1–3 pre-existing relationships to other roster NPCs — friendships, rivalries, or something more complicated — each with an LLM-written backstory note. Relationships also form and deepen through play: sharing a table strengthens a pair's bond (+5 per session), strangers who play together have a small chance of striking up a new relationship, and every 20 strength points the relationship's note is rewritten (and its type can shift — a friendship can sour into something complicated). When two related NPCs sit at the same table, both play with their shared history in their prompts, gated by SALOON_DETAIL_LEVEL: low omits it, medium injects the type and a one-sentence note, high the full note. Admins can inspect any NPC's relationships with /npcrelationships.
| Variable | Default | Description |
|---|---|---|
LLM_PROVIDER |
openai |
LLM provider: openai, claude, none (disables the LLM client entirely — bots always use basic strategy), or fake (deterministic offline provider for testing) |
ANTHROPIC_API_KEY |
— | API key for Claude; supports ANTHROPIC_API_KEY_FILE |
OPENAI_API_KEY |
— | API key for OpenAI; supports OPENAI_API_KEY_FILE |
LLM_MODEL |
provider default | Override model (claude-haiku-4-5 / gpt-4o-mini) |
LLM_TIMEOUT |
5 |
Seconds before falling back to basic strategy |
LLM_HEALTHCHECK_INTERVAL |
300 |
Seconds between periodic re-checks of the LLM provider (detects credit exhaustion/outages and recovery without a restart) |
LLM_DOWN_GRACE_PERIOD |
120 |
Seconds a provider must keep failing before it's reported down for alerting/metrics — absorbs transient blips (e.g. DNS not ready right after a restart) without paging |
LLM_SESSION_MEMORY_TIMEOUT |
15 |
Seconds allowed for the background session-memory call |
BLACKJACK_NPC_DEPARTURE_BASE |
0.02 |
Baseline per-hand chance an NPC calls it a night |
BLACKJACK_NPC_DEPARTURE_RAMP |
0.28 |
Extra departure chance once an NPC has seen a full session |
METRICS_PORT |
9400 |
Port for the Prometheus /metrics endpoint |
Exactly one provider is active at a time, chosen by LLM_PROVIDER (default openai) — the corresponding API key is required for openai/claude; an unset or invalid key (or LLM_PROVIDER=none) means bot players still join the game but use basic blackjack strategy instead of AI decisions. The provider is periodically re-checked while running, so credits running out or being topped up are picked up automatically.
All four secret variables (DISCORD_TOKEN, DISCORD_GUILDS, ANTHROPIC_API_KEY, OPENAI_API_KEY) resolve in priority order: direct env var → <VAR>_FILE path → /run/secrets/<lowercase_var> → unset. Docker secrets mounted at /run/secrets/ are picked up automatically with no extra configuration.
The server exposes a Prometheus-format /metrics endpoint on METRICS_PORT (default 9400): LLM call/token counters (saloonbot_llm_calls_total, saloonbot_llm_input_tokens_total, saloonbot_llm_output_tokens_total, labeled by purpose/model/provider) and provider health (saloonbot_llm_provider_up gauge, saloonbot_llm_provider_failures_total counter). This repo only exposes the metrics — scraping, dashboards, and alerting are configured in whatever external Prometheus/Grafana setup you point at it.
A standalone CLI client (cli.py) lets you play and test the game locally without Discord.
export REDIS_HOST=localhost REDIS_PORT=6379
python cli.pyAt startup you'll be prompted for your name and how many LLM bot players to add (0–4). The CLI auto-joins the game and drops you into the command prompt.
Available commands:
| Command | Description |
|---|---|
join |
Sit down at the table (done automatically at startup) |
bet <amount> |
Place a bet during the betting phase |
hit |
Draw another card |
stand |
Hold your current hand |
leave |
Leave the game |
addnpc [count] |
Add roster NPC(s) to the current game |
removenpc [name] |
Remove an NPC (by name, or any if omitted) |
help |
Show command list |
quit |
Exit |
SaloonBot provides flexible development workflows using Docker Compose configurations. The bot consists of two main components: the Discord bot (bot.py) and the server component (server.py), both communicating through Redis. The server persists state to a database — MySQL in production, SQLite locally.
The server supports SQLite as a drop-in replacement for MySQL, controlled by the USE_SQLITE environment variable. All three dev scripts enable this automatically, so no MySQL container is needed for local development.
To use SQLite manually:
export USE_SQLITE=1 # enables SQLite
export SQLITE_PATH=saloonbot.db # optional; this is the default
python server.pyThere are three development compose files, each designed for a different workflow:
compose.dev-bot-local.yml- Runs server + redis in Docker, allowing you to runbot.pylocallycompose.dev-server-local.yml- Runs bot + redis in Docker, allowing you to runserver.pylocallycompose.dev-redis-only.yml- Runs redis only in Docker, allowing you to run both components locally
For convenience, helper scripts are provided for each scenario:
./dev-bot.shThis starts the server and redis containers (server uses SQLite), then runs the bot locally. Requires DISCORD_TOKEN and DISCORD_GUILDS environment variables.
./dev-server.shThis starts the bot and redis containers, then runs the server locally with SQLite. Requires discord_token.txt and discord_guilds.txt files for the bot container.
./dev-redis.shThis starts redis in Docker. You can then run bot.py and server.py separately in different terminals:
# Terminal 1
export REDIS_HOST=localhost REDIS_PORT=6379 SALOONBOT_DEBUG=1
export DISCORD_TOKEN="your-token" DISCORD_GUILDS="your-guild-ids"
python bot.py
# Terminal 2
export REDIS_HOST=localhost REDIS_PORT=6379 USE_SQLITE=1 SALOONBOT_DEBUG=1
python server.pyYou can also use Docker Compose directly without the helper scripts:
# Start services
docker compose -f compose.dev-bot-local.yml up -d
# View logs
docker compose -f compose.dev-bot-local.yml logs -f
# Stop services
docker compose -f compose.dev-bot-local.yml downEnvironment-specific compose files are provided:
compose-production.yml— production deploymentcompose.staging.yml— staging deployment
Run the appropriate environment directly:
docker compose -f compose-production.yml up -d
docker compose -f compose.staging.yml up -dIf you always deploy the same environment on a given host, you can symlink compose.yml to avoid specifying -f every time:
# Example: always use production on this host
ln -s compose-production.yml compose.yml
docker compose up -dThis runs all components (bot, server, and redis) in Docker containers.
For staging hosts that should track main automatically, staging-autodeploy.sh
fast-forwards the checkout to origin/main and re-runs
docker compose -f compose.staging.yml up -d when anything changed. It's meant
to run from a systemd timer or cron entry, complementing watchtower: watchtower
auto-updates images but reuses each container's existing config, so compose-file
changes (ports, env vars, new services) only land via a pull + up.
Reference unit files are in systemd/ — install with:
sudo cp systemd/saloonbot-autodeploy.{service,timer} /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now saloonbot-autodeploy.timerSchema migrations run automatically on server startup — deploy new code and restart; no manual SQL needed.
Secrets are sourced from /etc/saloonbot/secrets/ on the host, which persists across reboots (unlike /run/secrets, which is a tmpfs and is wiped on restart). Run this once on each host:
mkdir -p /etc/saloonbot/secrets
chmod 700 /etc/saloonbot/secrets
# Required
echo -n "your-discord-token" > /etc/saloonbot/secrets/discord_token
echo -n "guild_id1,guild_id2" > /etc/saloonbot/secrets/discord_guilds
# Optional — LLM bot players. Create empty files if unused.
echo -n "your-anthropic-key" > /etc/saloonbot/secrets/anthropic_api_key
echo -n "" > /etc/saloonbot/secrets/openai_api_key
chmod 600 /etc/saloonbot/secrets/*All four secret files must exist (Docker Compose requires them even if empty). Docker mounts them into each container at /run/secrets/<name>, where SaloonBot picks them up automatically.
To run unit tests, run:
pytest test.py(python test.py also works, since it's a plain unittest suite.)
End-to-end tests validate the complete system integration using the Redis interface with real MySQL and Redis services (no mocking). These tests:
- Start Redis and MySQL via docker-compose
- Run the actual server process
- Simulate client interactions through Redis pub/sub
- Verify game logic, database persistence, and server behavior
To run end-to-end tests:
python test_e2e.pyOr use the helper script:
./run-e2e-tests.shRequirements:
- Docker and Docker Compose must be installed and running
- The tests will automatically start and stop the necessary services
Note: End-to-end tests take longer to run (~2 minutes) as they start/stop Docker containers and wait for game timing events.
hunt_flaky.py re-uses a single Docker stack across N runs per test case, making repeated flakiness detection roughly 100× faster than re-spinning containers each time.
source .venv/bin/activate
python hunt_flaky.py --runs 20Options: --runs N (default 10), --output FILE (JSON results), --class ClassName (limit to specific test class).