expanded basic game features

This commit is contained in:
2026-04-27 08:45:44 -07:00
parent 0217d6636f
commit 5ff452ba2c
21 changed files with 5001 additions and 172 deletions

18
activate_with_env.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Helper script to activate venv and set MariaDB environment variables
# Activate virtual environment
source .venv/bin/activate
# Find and set MARIADB_CONFIG
MARIADB_CONFIG_PATH=$(find /opt/homebrew -name mariadb_config 2>/dev/null | head -1)
if [ -n "$MARIADB_CONFIG_PATH" ]; then
export MARIADB_CONFIG="$MARIADB_CONFIG_PATH"
echo "✓ MARIADB_CONFIG set to: $MARIADB_CONFIG_PATH"
else
echo "⚠ Warning: mariadb_config not found. Install with: brew install mariadb-connector-c"
fi
echo "Virtual environment activated and ready to use!"

View File

@@ -1,3 +1,14 @@
def _coerce_int(val, default=0):
if val is None:
return default
if isinstance(val, bool):
return int(val)
try:
return int(val)
except (TypeError, ValueError):
return default
class Card: class Card:
def __init__(self): def __init__(self):
self.name = "" self.name = ""
@@ -81,10 +92,10 @@ class Citizen(Card):
self.gold_cost = gold_cost self.gold_cost = gold_cost
self.roll_match1 = roll_match1 self.roll_match1 = roll_match1
self.roll_match2 = roll_match2 self.roll_match2 = roll_match2
self.shadow_count = shadow_count self.shadow_count = _coerce_int(shadow_count)
self.holy_count = holy_count self.holy_count = _coerce_int(holy_count)
self.soldier_count = soldier_count self.soldier_count = _coerce_int(soldier_count)
self.worker_count = worker_count self.worker_count = _coerce_int(worker_count)
self.gold_payout_on_turn = gold_payout_on_turn self.gold_payout_on_turn = gold_payout_on_turn
self.gold_payout_off_turn = gold_payout_off_turn self.gold_payout_off_turn = gold_payout_off_turn
self.strength_payout_on_turn = strength_payout_on_turn self.strength_payout_on_turn = strength_payout_on_turn
@@ -112,6 +123,12 @@ class Citizen(Card):
"holy_count": self.holy_count, "holy_count": self.holy_count,
"soldier_count": self.soldier_count, "soldier_count": self.soldier_count,
"worker_count": self.worker_count, "worker_count": self.worker_count,
"roles": {
"shadow": self.shadow_count,
"holy": self.holy_count,
"soldier": self.soldier_count,
"worker": self.worker_count,
},
"gold_payout_on_turn": self.gold_payout_on_turn, "gold_payout_on_turn": self.gold_payout_on_turn,
"gold_payout_off_turn": self.gold_payout_off_turn, "gold_payout_off_turn": self.gold_payout_off_turn,
"strength_payout_on_turn": self.strength_payout_on_turn, "strength_payout_on_turn": self.strength_payout_on_turn,
@@ -132,10 +149,10 @@ class Citizen(Card):
gold_cost=dict_["gold_cost"], gold_cost=dict_["gold_cost"],
roll_match1=dict_["roll_match1"], roll_match1=dict_["roll_match1"],
roll_match2=dict_["roll_match2"], roll_match2=dict_["roll_match2"],
shadow_count=dict_["shadow_count"], shadow_count=dict_.get("shadow_count"),
holy_count=dict_["holy_count"], holy_count=dict_.get("holy_count"),
soldier_count=dict_["soldier_count"], soldier_count=dict_.get("soldier_count"),
worker_count=dict_["worker_count"], worker_count=dict_.get("worker_count"),
gold_payout_on_turn=dict_["gold_payout_on_turn"], gold_payout_on_turn=dict_["gold_payout_on_turn"],
gold_payout_off_turn=dict_["gold_payout_off_turn"], gold_payout_off_turn=dict_["gold_payout_off_turn"],
strength_payout_on_turn=dict_["strength_payout_on_turn"], strength_payout_on_turn=dict_["strength_payout_on_turn"],
@@ -157,10 +174,10 @@ class Domain(Card):
self.domain_id = domain_id self.domain_id = domain_id
self.name = name self.name = name
self.gold_cost = gold_cost self.gold_cost = gold_cost
self.shadow_count = shadow_count self.shadow_count = _coerce_int(shadow_count)
self.holy_count = holy_count self.holy_count = _coerce_int(holy_count)
self.soldier_count = soldier_count self.soldier_count = _coerce_int(soldier_count)
self.worker_count = worker_count self.worker_count = _coerce_int(worker_count)
self.vp_reward = vp_reward self.vp_reward = vp_reward
self.has_activation_effect = has_activation_effect self.has_activation_effect = has_activation_effect
self.has_passive_effect = has_passive_effect self.has_passive_effect = has_passive_effect
@@ -179,6 +196,12 @@ class Domain(Card):
"holy_count": self.holy_count, "holy_count": self.holy_count,
"soldier_count": self.soldier_count, "soldier_count": self.soldier_count,
"worker_count": self.worker_count, "worker_count": self.worker_count,
"roles": {
"shadow": self.shadow_count,
"holy": self.holy_count,
"soldier": self.soldier_count,
"worker": self.worker_count,
},
"vp_reward": self.vp_reward, "vp_reward": self.vp_reward,
"has_activation_effect": self.has_activation_effect, "has_activation_effect": self.has_activation_effect,
"has_passive_effect": self.has_passive_effect, "has_passive_effect": self.has_passive_effect,
@@ -194,10 +217,10 @@ class Domain(Card):
domain_id=dict_['domain_id'], domain_id=dict_['domain_id'],
name=dict_['name'], name=dict_['name'],
gold_cost=dict_['gold_cost'], gold_cost=dict_['gold_cost'],
shadow_count=dict_['shadow_count'], shadow_count=dict_.get('shadow_count'),
holy_count=dict_['holy_count'], holy_count=dict_.get('holy_count'),
soldier_count=dict_['soldier_count'], soldier_count=dict_.get('soldier_count'),
worker_count=dict_['worker_count'], worker_count=dict_.get('worker_count'),
vp_reward=dict_['vp_reward'], vp_reward=dict_['vp_reward'],
has_activation_effect=dict_['has_activation_effect'], has_activation_effect=dict_['has_activation_effect'],
has_passive_effect=dict_['has_passive_effect'], has_passive_effect=dict_['has_passive_effect'],

40
check_db_server.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""
Simple script to check if MariaDB/MySQL server is running
Doesn't require mariadb module - just checks if port is open
"""
import socket
import sys
def check_database_server():
"""Check if database server is listening on port 3306"""
print("Checking if MariaDB/MySQL server is accessible on localhost:3306...")
print("=" * 50)
print("(Make sure SSH port forwarding is active: ssh -L 3306:localhost:3306 lukesau.com)")
host = '127.0.0.1'
port = 3306
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex((host, port))
sock.close()
if result == 0:
print(f"✓ Database server is accessible at {host}:{port}")
return True
else:
print(f"✗ Cannot reach {host}:{port}")
print("\nMake sure SSH port forwarding is active:")
print(" ssh -L 3306:localhost:3306 lukesau.com")
return False
except Exception as e:
print(f"✗ Error checking server: {e}")
return False
if __name__ == "__main__":
success = check_database_server()
sys.exit(0 if success else 1)

13
docs/README.md Normal file
View File

@@ -0,0 +1,13 @@
# VCK Online Docs
This folder contains developer documentation for the VCK Online dev/test server and game engine.
## Start here
- `README_SERVER.md`: how to run the FastAPI server and use the dev HTML client
- `dev-setup.md`: local environment setup (venv + MariaDB connector)
- `database.md`: database expectations, SSH tunnel, and stored procedure setup
- `server.md`: FastAPI server architecture and API surface
- `game.md`: game engine model and the DB-backed game bootstrap flow
- `testing.md`: how to run the included test scripts

79
docs/README_SERVER.md Normal file
View File

@@ -0,0 +1,79 @@
# VCK Online FastAPI Server
Simple REST API server for developing and testing the Valeria Card Kingdoms Online game.
## Setup
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Make sure database is accessible (SSH tunnel if needed):
```bash
ssh -L 3306:localhost:3306 lukesau.com
```
## Running the Server
```bash
python3 server.py
```
Or with uvicorn directly:
```bash
uvicorn server:app --host 0.0.0.0 --port 8000 --reload
```
The server will start on `http://localhost:8000`
## API Endpoints
### Lobby
- `POST /api/lobby/join` - Join lobby with name
```json
{"name": "Player Name"}
```
Returns: `{"player_id": "...", "message": "Joined lobby"}`
- `POST /api/lobby/ready` - Mark player as ready
```json
{"player_id": "..."}
```
Returns game info if all players ready
- `POST /api/lobby/unready` - Mark player as not ready
- `POST /api/lobby/leave?player_id=...` - Leave lobby
- `GET /api/lobby/status?player_id=...` - Get lobby status
### Game
- `GET /api/game/{game_id}/state` - Get current game state
- `POST /api/game/{game_id}/action` - Perform game action
```json
{
"player_id": "...",
"action_type": "hire_citizen|buy_domain|slay_monster|act_on_required_action|roll_phase|harvest_phase|play_turn",
"citizen_id": 123, // for hire_citizen
"domain_id": 456, // for buy_domain
"monster_id": 789, // for slay_monster
"gold_cost": 5, // for hire_citizen/buy_domain
"strength_cost": 3, // for slay_monster
"magic_cost": 0, // optional
"action": "choose 1" // for act_on_required_action
}
```
## Web Client
Visit `http://localhost:8000` for a simple HTML client to test the API.
## Development Notes
- Games are stored in-memory (will be lost on server restart)
- Inactive games are cleaned up after 3 minutes of no activity
- Inactive lobby players are removed after 60 seconds
- This is a development/testing server, not production-ready

64
docs/database.md Normal file
View File

@@ -0,0 +1,64 @@
# Database
## Overview
The game bootstrap in `game.py` loads card data from a MariaDB database named `vckonline` using stored procedures to select card sets and randomize stacks.
The code assumes it can connect to:
- host: `127.0.0.1`
- port: `3306`
- user: `vckonline`
- password: `vckonline`
- database: `vckonline`
This is designed to work with an SSH tunnel that forwards the remote DB to local port 3306.
## SSH tunnel
Keep an SSH port forward running while using the DB locally:
```bash
ssh -L 3306:localhost:3306 lukesau.com
```
## Stored procedures
The server/game code expects these procedures to exist:
- `select_base1_monsters()`
- `select_base1_citizens()`
- `select_base2_monsters()`
- `select_base2_citizens()`
- `select_random_domains()`
- `select_random_dukes()`
To install all procedures:
```bash
./sql/run_sql.sh sql/create_all_stored_procedures.sql
```
See `sql/INSTALL_PROCEDURES.md` for additional options (mysql client, interactive MariaDB session, installing individually).
## User / grants setup
If you have authentication or permissions problems, use:
- `sql/USER_SETUP_GUIDE.md`: investigation and fix commands (create users for `localhost`, `127.0.0.1`, `%`, and grant privileges)
- `sql/fix_user_setup.sql`: a convenience SQL script in this repo (if you prefer to run a script vs copy/paste commands)
## Verifying the DB
Quick port-level check (no Python deps):
```bash
python3 check_db_server.py
```
Full end-to-end check (Python + DB + tables + stored procs):
```bash
python3 test_database.py
```

33
docs/dev-setup.md Normal file
View File

@@ -0,0 +1,33 @@
# Dev setup
## Python environment
This repo expects a Python venv in `.venv/`.
The easiest path on macOS is to use the helper script:
```bash
./setup_venv.sh
```
That script:
- Creates (or reuses) `.venv/`
- Tries to locate `mariadb_config` under `/opt/homebrew`
- Installs `mariadb-connector-c` via Homebrew if needed
- Exports `MARIADB_CONFIG` and runs `pip install -r requirements.txt`
If you already have `.venv/` created and just want to activate with the MariaDB environment variable:
```bash
source ./activate_with_env.sh
```
## Requirements
Dependencies are listed in `requirements.txt` and include:
- `fastapi` + `uvicorn` (API server)
- `mariadb` (DB access; requires MariaDB Connector/C to build)
- `shortuuid` (player id generation)

121
docs/game.md Normal file
View File

@@ -0,0 +1,121 @@
# Game engine (`game.py`)
## Key types
`game.py` defines the core runtime objects used by the server:
- `Game`: contains the current game board state and implements actions/phases
- `Player`: per-player scores and owned cards
- `LobbyMember` / `GameMember`: lightweight records used by the server for lobby/game membership
It also provides:
- `load_game_data(...)`: builds an initial `game_state` dict by pulling data from MariaDB and dealing stacks
- `SummaryEncoder` and `GameObjectEncoder`: JSON encoders used by the server to serialize game state
## Game lifecycle
At runtime (via the server):
- `server.py` calls `load_game_data(game_id, preset, game_gamers)` to create a starting `game_state`
- The server wraps that dict in a `Game` object: `Game(game_state)`
- The server exposes the game via `/api/game/{game_id}/state` and `/api/game/{game_id}/action`
The `Game` object tracks:
- `player_list`
- `monster_grid`, `citizen_grid`, `domain_grid`
- dice: `die_one`, `die_two`, `die_sum`
- `effects`
- `action_required` (used to block on per-player, sequential “choose …” actions)
- `concurrent_action` (used to block on non-ordered, multi-player prompts; see “Concurrent actions” below)
- `last_active_time` (used by the server for cleanup)
## DB-backed bootstrap (`load_game_data`)
`load_game_data` is responsible for:
- Fetching cards using stored procedures:
- citizens/monsters depend on the `preset` (e.g. `"base1"` / `"base2"`)
- domains and dukes are randomized via procedures
- starters are selected via a direct `SELECT * FROM starters`
- Creating `Player` instances from the lobby/game membership list
- Randomizing player order and dealing initial cards
- Dealing stacks onto the board:
- monsters grouped by area, then 5 areas selected
- citizens grouped by roll match, placed into 10 stacks (special-cased for roll 11)
- domains dealt into 5 stacks of 3, with the top visible/accessible
This function currently assumes local DB connectivity via `127.0.0.1:3306` (typically via SSH port forward).
See `docs/database.md` for DB setup and stored procedure installation.
## Actions & phases
The server routes map `action_type` strings to methods on `Game`.
Common paths:
- `roll_phase()` rolls two dice and computes a sum
- `harvest_phase()` pays out from owned starters/citizens for all players based on the roll
- `hire_citizen(...)`, `buy_domain(...)`, `slay_monster(...)` mutate board stacks and player resources
### “choose …” actions
Some special payouts set `action_required` and start a background thread that waits until `act_on_required_action` updates `action_required` with a choice.
This is a dev-oriented approach; it allows the REST API to supply a follow-up choice via `act_on_required_action` while the game engine waits.
## Concurrent actions (non-ordered prompts)
Some prompts are not turn-based: every participating player should be able to
respond at the same time, in any order, and the game must wait until **all**
of them have submitted before progressing. The starting duke selection is the
first example — every player simultaneously discards down to one of the dukes
they were dealt.
These are modeled with `Game.concurrent_action`, which is independent from
`action_required`:
```
concurrent_action = {
"kind": "choose_duke", # routes to a handler in CONCURRENT_HANDLERS
"pending": ["pid1", "pid3"],
"completed": ["pid2"],
"responses": { "pid2": <opaque payload> },
"data": { ... } # handler-specific extras (often empty)
}
```
Engine semantics:
- While `concurrent_action.pending` is non-empty, `advance_tick()` returns
`False`. No phase transitions happen, no harvest progresses, and no
per-player turn actions are accepted. `is_blocked_on_concurrent_action()`
exposes the same predicate.
- Players submit via `Game.submit_concurrent_action(player_id, response, kind=...)`.
The handler's `apply()` validates and applies that player's response
immediately (so per-player effects don't have to wait for the others).
- When the last pending player submits, the handler's `finalize()` runs
(for any cross-player resolution), `concurrent_action` is cleared, and
if the engine was sitting in setup it advances forward.
### Adding a new concurrent action kind
1. Implement a handler class with:
- `apply(self, game, player_id, response)` — validate + apply per-player
side effects. Raise `ValueError` to reject a submission (the player
stays in `pending`).
- `finalize(self, game)` — optional; runs once after every participant
has submitted.
2. Register it in `CONCURRENT_HANDLERS` keyed by `kind`.
3. Build the prompt with `_new_concurrent_action(kind, participant_ids, data=...)`
and assign it to `game.concurrent_action` at the point in the engine
where the gate should appear.
4. On the client, register a renderer in the `CONCURRENT_RENDERERS` map in
the dev client (server.py HTML) keyed on the same `kind`.
Because the engine itself only knows "block while pending is non-empty",
the concurrent gate is fully reusable — no engine changes are required to
add new kinds (mulligan, simultaneous discard, voting, etc.).

88
docs/server.md Normal file
View File

@@ -0,0 +1,88 @@
# Server (`server.py`)
## What it is
`server.py` is a FastAPI development server that:
- Maintains an in-memory lobby (`lobby`)
- Starts games when all lobby players are ready
- Stores active games in-memory (`games`)
- Exposes REST endpoints for lobby operations and game actions
- Serves a simple HTML test client at `/`
This server is intended for development/testing, not production.
## In-memory state
The server keeps three top-level collections:
- `lobby`: list of `LobbyMember` (players waiting to start a game)
- `games`: dict of `game_id -> Game`
- `gamers`: list of `GameMember` (player_id/name/game_id records for in-game players)
There is no persistence; restarting the server resets everything.
## Lobby flow
High-level flow:
- `POST /api/lobby/join`: creates a `LobbyMember` with a `shortuuid` `player_id`
- `POST /api/lobby/ready`: marks the player ready; when all lobby players are ready (and there are at least 2), it starts a game:
- generates a new `game_id` (uuid4)
- moves ready lobby members into `gamers` for that `game_id`
- calls `load_game_data(game_id, "base1", game_gamers)` and constructs `Game(game_state)`
- `GET /api/lobby/status`: returns lobby members + whether the requesting player is already in a game
Lobby cleanup:
- `GET /api/lobby/status` prunes lobby members inactive for > 60 seconds
## Game API
- `GET /api/game/{game_id}/state`: returns the current game state encoded using `GameObjectEncoder` (from `game.py`)
- `POST /api/game/{game_id}/action`: performs a game action and returns the updated game state
Supported `action_type` values currently include:
- `hire_citizen`
- `buy_domain`
- `slay_monster`
- `take_resource`
- `harvest_card`
- `act_on_required_action` (sequential, single-player follow-ups)
- `submit_concurrent_action` (non-ordered, multi-player prompts; see below)
- `roll_phase`
- `harvest_phase`
- `play_turn`
### `submit_concurrent_action`
Used to respond to a `concurrent_action` gate (see `docs/game.md`). The
serialized game state exposes a `concurrent_action` object with `kind`,
`pending`, and `completed` lists; while `pending` is non-empty no other
turn-based action will succeed. Request body:
```
{
"player_id": "<pid>",
"action_type": "submit_concurrent_action",
"kind": "choose_duke", // optional sanity check
"response": "<opaque string>" // handler-specific payload
}
```
The server validates that the player is in `pending` and that `kind`
(if provided) matches the active gate. Players may submit in any order;
when the last pending player submits, the engine auto-advances out of
the setup gate.
## Game cleanup
On startup, a background task deletes games inactive for > 180 seconds and prunes the corresponding `gamers` entries.
## Dev HTML client
The root route `/` serves a simple HTML page that calls the lobby endpoints and can fetch a game state.
For run instructions, see `docs/README_SERVER.md`.

31
docs/testing.md Normal file
View File

@@ -0,0 +1,31 @@
# Testing & diagnostics
## Database connectivity
### Port-level check
`check_db_server.py` checks whether `127.0.0.1:3306` is reachable (useful to verify your SSH tunnel).
```bash
python3 check_db_server.py
```
### End-to-end DB validation
`test_database.py` does a more complete validation:
- imports `mariadb`
- connects to the DB
- checks required tables exist
- prints row counts and card contents (citizens/monsters/domains/dukes/starters)
- checks required stored procedures exist
- calls stored procedures and prints returned rows
```bash
python3 test_database.py
```
## API server smoke test
See `docs/README_SERVER.md` to run the FastAPI server and use the built-in HTML client at `/`.

1361
game.py

File diff suppressed because it is too large Load Diff

22
requirements.txt Normal file
View File

@@ -0,0 +1,22 @@
# Core database connection
# NOTE: mariadb package requires MariaDB Connector/C to be installed first
#
# Installation:
# 1. Install MariaDB Connector/C:
# macOS: brew install mariadb-connector-c
# Linux: sudo apt-get install libmariadb-dev (Debian/Ubuntu) or equivalent
#
# 2. Set MARIADB_CONFIG environment variable before pip install:
# export MARIADB_CONFIG="/opt/homebrew/Cellar/mariadb-connector-c/3.4.8/bin/mariadb_config"
# (or run: ./setup_venv.sh which does this automatically)
#
# 3. Then install: pip install -r requirements.txt
mariadb>=1.1.0
# UUID generation
shortuuid>=1.0.0
# Web framework for API server
fastapi>=0.104.0
uvicorn>=0.24.0

2382
server.py Normal file

File diff suppressed because it is too large Load Diff

40
setup_venv.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Setup script for VCK Online virtual environment
# Find mariadb_config
MARIADB_CONFIG_PATH=$(find /opt/homebrew -name mariadb_config 2>/dev/null | head -1)
if [ -z "$MARIADB_CONFIG_PATH" ]; then
echo "Error: mariadb_config not found. Installing mariadb-connector-c..."
brew install mariadb-connector-c
MARIADB_CONFIG_PATH=$(find /opt/homebrew -name mariadb_config 2>/dev/null | head -1)
fi
if [ -z "$MARIADB_CONFIG_PATH" ]; then
echo "Error: Could not find mariadb_config after installation."
echo "Please install manually: brew install mariadb-connector-c"
exit 1
fi
echo "Found mariadb_config at: $MARIADB_CONFIG_PATH"
echo "Setting MARIADB_CONFIG environment variable..."
# Activate virtual environment if it exists
if [ -d ".venv" ]; then
source .venv/bin/activate
echo "Virtual environment activated"
else
echo "Creating virtual environment..."
python3 -m venv .venv
source .venv/bin/activate
fi
# Set environment variable and install packages
export MARIADB_CONFIG="$MARIADB_CONFIG_PATH"
pip install -r requirements.txt
echo ""
echo "Setup complete! To activate the environment in the future:"
echo " source .venv/bin/activate"
echo " export MARIADB_CONFIG=\"$MARIADB_CONFIG_PATH\""

107
sql/INSTALL_PROCEDURES.md Normal file
View File

@@ -0,0 +1,107 @@
# Installing Stored Procedures
All stored procedure SQL files are ready to use. You have several options:
## Prerequisites
1. **SSH Port Forwarding** - Make sure you have an active SSH tunnel:
```bash
ssh -L 3306:localhost:3306 lukesau.com
```
Keep this terminal open while running SQL commands.
2. **MySQL Client** - Install if needed:
```bash
brew install mysql-client
```
To add mysql-client to your PATH permanently, add to `~/.zshrc`:
```bash
echo 'export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
```
## Option 1: Use the Helper Script (Easiest)
```bash
./sql/run_sql.sh sql/create_all_stored_procedures.sql
```
## Option 2: Use MySQL Client Directly
If mysql is in your PATH (added to ~/.zshrc), you can use:
```bash
mysql -h 127.0.0.1 -P 3306 -u vckonline -p vckonline < sql/create_all_stored_procedures.sql
```
Or use the full path:
```bash
/opt/homebrew/opt/mysql-client/bin/mysql -h 127.0.0.1 -P 3306 -u vckonline -p vckonline < sql/create_all_stored_procedures.sql
```
**Note:** The `-h 127.0.0.1 -P 3306` flags connect through your SSH tunnel to the remote database.
## Option 3: Interactive MariaDB Session
If you're already logged into MariaDB on the server:
```sql
source sql/create_all_stored_procedures.sql;
```
## Option 4: Install Individually
Run each procedure file separately using the helper script:
```bash
./sql/run_sql.sh sql/select_base1_citizens_sp.sql
./sql/run_sql.sh sql/select_base1_monsters_sp.sql
./sql/run_sql.sh sql/select_base2_citizens_sp.sql
./sql/run_sql.sh sql/select_base2_monsters_sp.sql
./sql/run_sql.sh sql/select_random_domains_sp.sql
./sql/run_sql.sh sql/select_random_dukes_sp.sql
```
Or using mysql client directly:
```bash
/opt/homebrew/Cellar/mysql-client/9.5.0/bin/mysql -h 127.0.0.1 -P 3306 -u vckonline -p vckonline < sql/select_base1_citizens_sp.sql
# ... repeat for each file
```
Or interactively in MariaDB on the server:
```sql
source sql/select_base1_citizens_sp.sql;
source sql/select_base1_monsters_sp.sql;
source sql/select_base2_citizens_sp.sql;
source sql/select_base2_monsters_sp.sql;
source sql/select_random_domains_sp.sql;
source sql/select_random_dukes_sp.sql;
```
## Verify Installation
After installing, verify with:
```sql
SHOW PROCEDURE STATUS WHERE Db = 'vckonline';
```
Or run the test script:
```bash
python3 test_database.py
```
## What Each Procedure Does
- **select_base1_citizens()** - Returns all citizens from base game 1
- **select_base1_monsters()** - Returns all monsters from base game 1
- **select_base2_citizens()** - Returns base game 2 citizens + Peasant and Knight from base1
- **select_base2_monsters()** - Returns base game 2 monsters + 2 random areas from base1
- **select_random_domains()** - Returns 15 random domains
- **select_random_dukes()** - Returns all dukes in random order

129
sql/USER_SETUP_GUIDE.md Normal file
View File

@@ -0,0 +1,129 @@
# MariaDB User Setup Guide for VCK Online
Run these commands interactively in MariaDB as root to investigate and fix the user setup.
## Investigation Commands
### 1. Check if the database exists
```sql
SHOW DATABASES LIKE 'vckonline';
```
### 2. Check if the user exists and from which hosts
```sql
SELECT User, Host FROM mysql.user WHERE User = 'vckonline';
```
### 3. Check current privileges (if user exists)
```sql
SHOW GRANTS FOR 'vckonline'@'localhost';
SHOW GRANTS FOR 'vckonline'@'127.0.0.1';
SHOW GRANTS FOR 'vckonline'@'%';
```
### 4. Check if database has data (if database exists)
```sql
USE vckonline;
SHOW TABLES;
```
### 5. Verify data exists in key tables
```sql
SELECT COUNT(*) AS citizens_count FROM citizens;
SELECT COUNT(*) AS monsters_count FROM monsters;
SELECT COUNT(*) AS domains_count FROM domains;
SELECT COUNT(*) AS dukes_count FROM dukes;
SELECT COUNT(*) AS starters_count FROM starters;
```
## Fix Commands
### Scenario A: User doesn't exist - Create new user
```sql
-- Create user for localhost connections
CREATE USER 'vckonline'@'localhost' IDENTIFIED BY 'vckonline';
-- Create user for 127.0.0.1 connections (SSH tunnel)
CREATE USER 'vckonline'@'127.0.0.1' IDENTIFIED BY 'vckonline';
-- Create user for remote connections (optional, for direct connections)
CREATE USER 'vckonline'@'%' IDENTIFIED BY 'vckonline';
-- Grant privileges
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'localhost';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'127.0.0.1';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'%';
-- Apply changes
FLUSH PRIVILEGES;
```
### Scenario B: User exists but password is wrong - Reset password
```sql
-- Reset password for existing user
ALTER USER 'vckonline'@'localhost' IDENTIFIED BY 'vckonline';
ALTER USER 'vckonline'@'127.0.0.1' IDENTIFIED BY 'vckonline';
ALTER USER 'vckonline'@'%' IDENTIFIED BY 'vckonline';
-- Ensure privileges are granted
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'localhost';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'127.0.0.1';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'%';
-- Apply changes
FLUSH PRIVILEGES;
```
### Scenario C: User exists but lacks privileges - Grant privileges
```sql
-- Grant privileges
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'localhost';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'127.0.0.1';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'%';
-- Apply changes
FLUSH PRIVILEGES;
```
## Verification
After running the fix commands, verify the setup:
```sql
-- Check user exists
SELECT User, Host FROM mysql.user WHERE User = 'vckonline';
-- Check privileges
SHOW GRANTS FOR 'vckonline'@'localhost';
SHOW GRANTS FOR 'vckonline'@'127.0.0.1';
-- Test connection (from another terminal, not in MariaDB)
-- mysql -u vckonline -p vckonline
-- Password: vckonline
```
## Quick Fix (All-in-One)
If you're confident the database exists with data, run this complete setup:
```sql
-- Create users if they don't exist (will error if they do, that's OK)
CREATE USER IF NOT EXISTS 'vckonline'@'localhost' IDENTIFIED BY 'vckonline';
CREATE USER IF NOT EXISTS 'vckonline'@'127.0.0.1' IDENTIFIED BY 'vckonline';
CREATE USER IF NOT EXISTS 'vckonline'@'%' IDENTIFIED BY 'vckonline';
-- Grant privileges (safe to run even if already granted)
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'localhost';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'127.0.0.1';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'%';
-- Apply changes
FLUSH PRIVILEGES;
-- Verify
SHOW GRANTS FOR 'vckonline'@'localhost';
```

View File

@@ -1,6 +1,6 @@
INSERT INTO vckonline.citizens (name,gold_cost,roll_match1,roll_match2,shadow_count,holy_count,soldier_count,worker_count,gold_payout_on_turn,gold_payout_off_turn,strength_payout_on_turn,strength_payout_off_turn,magic_payout_on_turn,magic_payout_off_turn,has_special_payout_on_turn,has_special_payout_off_turn,special_payout_on_turn,special_payout_off_turn,special_citizen,image) VALUES INSERT INTO vckonline.citizens (name,gold_cost,roll_match1,roll_match2,shadow_count,holy_count,soldier_count,worker_count,gold_payout_on_turn,gold_payout_off_turn,strength_payout_on_turn,strength_payout_off_turn,magic_payout_on_turn,magic_payout_off_turn,has_special_payout_on_turn,has_special_payout_off_turn,special_payout_on_turn,special_payout_off_turn,special_citizen,image) VALUES
('Cleric',3,1,0,0,1,0,0,0,0,0,0,3,1,0,0,NULL,NULL,0,NULL), ('Cleric',3,1,0,0,1,0,0,0,0,0,0,3,1,0,0,NULL,NULL,0,NULL),
('Merchant',2,2,0,0,0,0,1,0,1,0,0,0,0,1,0,NULL,NULL,0,NULL), ('Merchant',2,2,0,0,0,0,1,1,0,0,0,0,0,1,0,NULL,NULL,0,NULL),
('Mercenary',3,3,0,1,0,0,0,1,0,1,0,0,0,0,1,NULL,NULL,0,NULL), ('Mercenary',3,3,0,1,0,0,0,1,0,1,0,0,0,0,1,NULL,NULL,0,NULL),
('Archer',4,4,0,0,0,1,0,0,0,2,1,0,0,0,0,NULL,NULL,0,NULL), ('Archer',4,4,0,0,0,1,0,0,0,2,1,0,0,0,0,NULL,NULL,0,NULL),
('Peasant',2,5,0,0,0,0,1,1,1,0,0,0,0,0,0,NULL,NULL,0,NULL), ('Peasant',2,5,0,0,0,0,1,1,1,0,0,0,0,0,0,NULL,NULL,0,NULL),

View File

@@ -0,0 +1,76 @@
-- Create all stored procedures for VCK Online
-- Run this file as the vckonline user (or any user with CREATE ROUTINE privilege on vckonline database)
-- Usage: mysql -u vckonline -p vckonline < create_all_stored_procedures.sql
-- Or interactively: source create_all_stored_procedures.sql;
DELIMITER //
-- Drop existing procedures if they exist (to allow re-running this script)
DROP PROCEDURE IF EXISTS select_base1_citizens //
DROP PROCEDURE IF EXISTS select_base1_monsters //
DROP PROCEDURE IF EXISTS select_base2_citizens //
DROP PROCEDURE IF EXISTS select_base2_monsters //
DROP PROCEDURE IF EXISTS select_random_domains //
DROP PROCEDURE IF EXISTS select_random_dukes //
-- Base 1 Citizens
CREATE PROCEDURE select_base1_citizens()
BEGIN
SELECT * FROM citizens WHERE expansion = "base1";
END //
-- Base 1 Monsters
CREATE PROCEDURE select_base1_monsters()
BEGIN
SELECT * FROM monsters WHERE expansion = "base1";
END //
-- Base 2 Citizens
CREATE PROCEDURE select_base2_citizens()
BEGIN
SELECT * FROM citizens WHERE expansion = "base2"
UNION
SELECT * FROM citizens WHERE expansion = "base1" AND name IN ('Peasant', 'Knight');
END //
-- Base 2 Monsters
CREATE PROCEDURE select_base2_monsters()
BEGIN
DECLARE chosen_area1 VARCHAR(255);
DECLARE chosen_area2 VARCHAR(255);
SET chosen_area1 = (
SELECT area FROM monsters WHERE expansion = 'base1' GROUP BY area ORDER BY RAND() LIMIT 1
);
SET chosen_area2 = (
SELECT area FROM monsters WHERE expansion = 'base1' AND area <> chosen_area1 ORDER BY RAND() LIMIT 1
);
SELECT id_monsters, name, area, monster_type, monster_order,
strength_cost, magic_cost, vp_reward, gold_reward, strength_reward, magic_reward,
has_special_reward, special_reward, has_special_cost, special_cost, is_extra, expansion
FROM monsters
WHERE expansion = 'base2'
UNION
SELECT id_monsters, name, area, monster_type, monster_order,
strength_cost, magic_cost, vp_reward, gold_reward, strength_reward, magic_reward,
has_special_reward, special_reward, has_special_cost, special_cost, is_extra, expansion
FROM monsters
WHERE expansion = 'base1' AND area IN (chosen_area1, chosen_area2);
END //
-- Random Domains
CREATE PROCEDURE select_random_domains()
BEGIN
SELECT * FROM domains ORDER BY RAND() LIMIT 15;
END //
-- Random Dukes
CREATE PROCEDURE select_random_dukes()
BEGIN
SELECT * FROM dukes ORDER BY RAND();
END //
DELIMITER ;
-- Verify procedures were created
SHOW PROCEDURE STATUS WHERE Db = 'vckonline';

56
sql/fix_user_setup.sql Normal file
View File

@@ -0,0 +1,56 @@
-- ============================================
-- VCK Online Database User Setup Commands
-- Run these interactively in MariaDB as root
-- ============================================
-- Step 1: Check if the database exists
SHOW DATABASES LIKE 'vckonline';
-- Step 2: Check if the user exists
SELECT User, Host FROM mysql.user WHERE User = 'vckonline';
-- Step 3: Check current user privileges (if user exists)
SHOW GRANTS FOR 'vckonline'@'localhost';
SHOW GRANTS FOR 'vckonline'@'%';
-- Step 4: Check what tables exist in the database (if it exists)
USE vckonline;
SHOW TABLES;
-- Step 5: Check table row counts to verify data exists
SELECT
'citizens' AS table_name, COUNT(*) AS row_count FROM citizens
UNION ALL
SELECT 'monsters', COUNT(*) FROM monsters
UNION ALL
SELECT 'domains', COUNT(*) FROM domains
UNION ALL
SELECT 'dukes', COUNT(*) FROM dukes
UNION ALL
SELECT 'starters', COUNT(*) FROM starters;
-- ============================================
-- FIX COMMANDS (run only if needed)
-- ============================================
-- Option A: If user doesn't exist, create it
-- CREATE USER 'vckonline'@'localhost' IDENTIFIED BY 'vckonline';
-- CREATE USER 'vckonline'@'127.0.0.1' IDENTIFIED BY 'vckonline';
-- CREATE USER 'vckonline'@'%' IDENTIFIED BY 'vckonline';
-- Option B: If user exists but password is wrong, reset it
-- ALTER USER 'vckonline'@'localhost' IDENTIFIED BY 'vckonline';
-- ALTER USER 'vckonline'@'127.0.0.1' IDENTIFIED BY 'vckonline';
-- ALTER USER 'vckonline'@'%' IDENTIFIED BY 'vckonline';
-- Grant all privileges on vckonline database
-- GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'localhost';
-- GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'127.0.0.1';
-- GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'%';
-- Flush privileges to apply changes
-- FLUSH PRIVILEGES;
-- Verify the grants after creating/fixing
-- SHOW GRANTS FOR 'vckonline'@'localhost';

63
sql/run_sql.sh Executable file
View File

@@ -0,0 +1,63 @@
#!/bin/bash
# Helper script to run SQL files on remote database through SSH port forwarding
# Usage: ./sql/run_sql.sh sql/your_file.sql
# Or: bash sql/run_sql.sh sql/your_file.sql
# Find mysql binary (try PATH first, then specific location)
if command -v mysql >/dev/null 2>&1; then
MYSQL_BIN="mysql"
elif [ -f "/opt/homebrew/opt/mysql-client/bin/mysql" ]; then
MYSQL_BIN="/opt/homebrew/opt/mysql-client/bin/mysql"
elif [ -f "/opt/homebrew/Cellar/mysql-client/9.5.0/bin/mysql" ]; then
MYSQL_BIN="/opt/homebrew/Cellar/mysql-client/9.5.0/bin/mysql"
else
echo "Error: mysql binary not found"
echo "Please install mysql-client: brew install mysql-client"
echo "Or add to PATH: export PATH=\"/opt/homebrew/opt/mysql-client/bin:\$PATH\""
exit 1
fi
# Check if SQL file is provided
if [ -z "$1" ]; then
echo "Usage: $0 <sql_file>"
echo "Example: $0 sql/create_all_stored_procedures.sql"
exit 1
fi
SQL_FILE="$1"
# Check if SQL file exists
if [ ! -f "$SQL_FILE" ]; then
echo "Error: SQL file not found: $SQL_FILE"
exit 1
fi
# Check if port 3306 is accessible (SSH tunnel check)
if ! nc -z 127.0.0.1 3306 2>/dev/null; then
echo "Warning: Cannot connect to localhost:3306"
echo "Make sure SSH port forwarding is active:"
echo " ssh -L 3306:localhost:3306 lukesau.com"
echo ""
read -p "Continue anyway? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# Run the SQL file
echo "Running SQL file: $SQL_FILE"
echo "Connecting to database through SSH tunnel (localhost:3306)..."
echo ""
"$MYSQL_BIN" -h 127.0.0.1 -P 3306 -u vckonline -p vckonline < "$SQL_FILE"
if [ $? -eq 0 ]; then
echo ""
echo "✓ SQL file executed successfully"
else
echo ""
echo "✗ Error executing SQL file"
exit 1
fi

359
test_database.py Normal file
View File

@@ -0,0 +1,359 @@
#!/usr/bin/env python3
"""
Test script to check MariaDB database connection and status for VCK Online
"""
import sys
def test_database_connection():
"""Test the database connection and basic functionality"""
# Database connection parameters from game.py
# Using localhost for SSH port forwarding (ssh -L 3306:localhost:3306 lukesau.com)
db_config = {
'user': 'vckonline',
'password': 'vckonline',
'host': '127.0.0.1',
'database': 'vckonline'
}
print("Testing VCK Online Database Connection")
print("=" * 50)
# Test 1: Check if mariadb module is available
print("\n1. Checking mariadb module...")
try:
import mariadb
print(" ✓ mariadb module found")
except ImportError:
print(" ✗ mariadb module not found")
print(" Install with: pip install mariadb")
print(" Or with user flag: pip install --user mariadb")
print(" Or use virtualenv: python3 -m venv .env && source .env/bin/activate && pip install mariadb")
return False
# Test 1.5: Check if database server is accessible
print("\n1.5. Checking if database server is accessible on localhost:3306...")
print(" (Make sure SSH port forwarding is active: ssh -L 3306:localhost:3306 lukesau.com)")
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex(('127.0.0.1', 3306))
sock.close()
if result == 0:
print(" ✓ Database server is accessible on localhost:3306")
else:
print(" ✗ Cannot reach localhost:3306")
print("\n Make sure SSH port forwarding is active:")
print(" ssh -L 3306:localhost:3306 lukesau.com")
return False
# Test 2: Test connection
print("\n2. Testing database connection...")
try:
connection = mariadb.connect(**db_config)
print(f" ✓ Successfully connected to database '{db_config['database']}'")
cursor = connection.cursor(dictionary=True)
except mariadb.Error as e:
print(f" ✗ Connection failed: {e}")
print("\n Possible issues:")
print(" - Database 'vckonline' does not exist")
print(" - User 'vckonline' does not exist or password is incorrect")
print(" - User does not have permission to access from this IP")
return False
# Test 3: Check if required tables exist
print("\n3. Checking required tables...")
required_tables = ['citizens', 'monsters', 'domains', 'dukes', 'starters']
existing_tables = []
try:
cursor.execute("SHOW TABLES")
# SHOW TABLES returns a dict with key like 'Tables_in_vckonline'
tables = [list(row.values())[0] for row in cursor.fetchall()]
for table in required_tables:
if table in tables:
print(f" ✓ Table '{table}' exists")
existing_tables.append(table)
else:
print(f" ✗ Table '{table}' is missing")
except mariadb.Error as e:
print(f" ✗ Error checking tables: {e}")
connection.close()
return False
# Test 4: Check table row counts
print("\n4. Checking table data...")
for table in existing_tables:
try:
cursor.execute(f"SELECT COUNT(*) as count FROM {table}")
count = cursor.fetchone()['count']
print(f" {table}: {count} rows")
except mariadb.Error as e:
print(f" ✗ Error counting rows in '{table}': {e}")
# Test 4.5: Display all card data
print("\n4.5. Displaying all card data...")
# Citizens
try:
cursor.execute("SELECT name, gold_cost, roll_match1, roll_match2, shadow_count, holy_count, soldier_count, worker_count, expansion FROM citizens ORDER BY expansion, roll_match1")
citizens = cursor.fetchall()
print(f"\n CITIZENS ({len(citizens)} total):")
for c in citizens:
name, gc, r1, r2, sh, ho, so, wo, exp = c
roles = []
if sh: roles.append(f"{sh} Shadow")
if ho: roles.append(f"{ho} Holy")
if so: roles.append(f"{so} Soldier")
if wo: roles.append(f"{wo} Worker")
role_str = ", ".join(roles) if roles else "No roles"
roll_str = f"{r1}" + (f"/{r2}" if r2 else "")
print(f" {name:20} | Cost: {gc:2}gp | Roll: {roll_str:5} | {role_str:20} | {exp}")
except mariadb.Error as e:
print(f" ✗ Error fetching citizens: {e}")
# Monsters
try:
cursor.execute("SELECT name, area, monster_type, monster_order, strength_cost, magic_cost, vp_reward, gold_reward, strength_reward, magic_reward, expansion FROM monsters ORDER BY area, monster_order")
monsters = cursor.fetchall()
print(f"\n MONSTERS ({len(monsters)} total):")
for m in monsters:
name, area, mtype, order, sc, mc, vp, gr, sr, mr, exp = m
cost_str = f"{sc}sp" + (f" + {mc}mp" if mc else "")
reward_str = f"{vp}vp" + (f" + {gr}gp" if gr else "") + (f" + {sr}sp" if sr else "") + (f" + {mr}mp" if mr else "")
print(f" {name:25} | {area:10} | {mtype:8} | Cost: {cost_str:10} | Reward: {reward_str:15} | {exp}")
except mariadb.Error as e:
print(f" ✗ Error fetching monsters: {e}")
# Domains
try:
cursor.execute("SELECT name, gold_cost, shadow_count, holy_count, soldier_count, worker_count, vp_reward, text, expansion FROM domains ORDER BY expansion, gold_cost")
domains = cursor.fetchall()
print(f"\n DOMAINS ({len(domains)} total):")
for d in domains:
name, gc, sh, ho, so, wo, vp, text, exp = d
roles = []
if sh: roles.append(f"{sh} Shadow")
if ho: roles.append(f"{ho} Holy")
if so: roles.append(f"{so} Soldier")
if wo: roles.append(f"{wo} Worker")
role_str = ", ".join(roles) if roles else "No roles"
text_preview = (text[:40] + "...") if text and len(text) > 40 else (text or "No effect")
print(f" {name:25} | Cost: {gc:2}gp | {role_str:20} | {vp}vp | {text_preview} | {exp}")
except mariadb.Error as e:
print(f" ✗ Error fetching domains: {e}")
# Dukes
try:
cursor.execute("SELECT name, gold_mult, strength_mult, magic_mult, shadow_mult, holy_mult, soldier_mult, worker_mult, monster_mult, citizen_mult, domain_mult, expansion FROM dukes ORDER BY expansion, name")
dukes = cursor.fetchall()
print(f"\n DUKES ({len(dukes)} total):")
for d in dukes:
name, gm, sm, mm, shm, hom, som, wom, mom, cm, dom, exp = d
mults = []
if gm: mults.append(f"Gold×{gm}")
if sm: mults.append(f"Str×{sm}")
if mm: mults.append(f"Mag×{mm}")
if shm: mults.append(f"Shadow×{shm}")
if hom: mults.append(f"Holy×{hom}")
if som: mults.append(f"Soldier×{som}")
if wom: mults.append(f"Worker×{wom}")
if mom: mults.append(f"Monster×{mom}")
if cm: mults.append(f"Citizen×{cm}")
if dom: mults.append(f"Domain×{dom}")
mult_str = ", ".join(mults) if mults else "No multipliers"
print(f" {name:30} | {mult_str} | {exp}")
except mariadb.Error as e:
print(f" ✗ Error fetching dukes: {e}")
# Starters
try:
cursor.execute("SELECT name, roll_match1, roll_match2, gold_payout_on_turn, gold_payout_off_turn, strength_payout_on_turn, strength_payout_off_turn, magic_payout_on_turn, magic_payout_off_turn, expansion FROM starters ORDER BY roll_match1")
starters = cursor.fetchall()
print(f"\n STARTERS ({len(starters)} total):")
for s in starters:
name, r1, r2, gpot, gpoff, spot, spoff, mpot, mpoff, exp = s
roll_str = f"{r1}" + (f"/{r2}" if r2 else "")
payouts = []
if gpot: payouts.append(f"{gpot}gp (on)")
if gpoff: payouts.append(f"{gpoff}gp (off)")
if spot: payouts.append(f"{spot}sp (on)")
if spoff: payouts.append(f"{spoff}sp (off)")
if mpot: payouts.append(f"{mpot}mp (on)")
if mpoff: payouts.append(f"{mpoff}mp (off)")
payout_str = ", ".join(payouts) if payouts else "No payouts"
print(f" {name:20} | Roll: {roll_str:5} | {payout_str} | {exp}")
except mariadb.Error as e:
print(f" ✗ Error fetching starters: {e}")
# Test 5: Test stored procedures
print("\n5. Checking stored procedures...")
print(" (These are helper functions used by the game code to select cards)")
required_procedures = [
'select_base1_citizens',
'select_base1_monsters',
'select_base2_citizens',
'select_base2_monsters',
'select_random_domains',
'select_random_dukes'
]
try:
cursor.execute("SHOW PROCEDURE STATUS WHERE Db = 'vckonline'")
procedures = [row['Name'] for row in cursor.fetchall()]
missing_procedures = []
for proc in required_procedures:
if proc in procedures:
print(f" ✓ Procedure '{proc}' exists")
else:
print(f" ✗ Procedure '{proc}' is missing")
missing_procedures.append(proc)
if missing_procedures:
print(f"\n Note: {len(missing_procedures)} stored procedures are missing.")
print(" These can be created from the SQL files in the sql/ directory:")
print(" - select_base1_citizens_sp.sql")
print(" - select_base1_monsters_sp.sql")
print(" - select_base2_citizens_sp.sql")
print(" - select_base2_monsters_sp.sql")
print(" - select_random_domains_sp.sql")
print(" - select_random_dukes_sp.sql")
except mariadb.Error as e:
print(f" ✗ Error checking procedures: {e}")
# Test 6: Test a sample query
print("\n6. Testing sample query...")
try:
cursor.execute("SELECT * FROM starters LIMIT 1")
result = cursor.fetchone()
if result:
name = result.get('name', 'N/A')
print(f" ✓ Sample query successful (found starter: {name})")
else:
print(" ⚠ Sample query returned no results (table may be empty)")
except mariadb.Error as e:
print(f" ✗ Sample query failed: {e}")
# Test 7: Test all stored procedures and display results
print("\n7. Testing stored procedures and displaying results...")
# Get list of available procedures
try:
cursor.execute("SHOW PROCEDURE STATUS WHERE Db = 'vckonline'")
available_procedures = {row['Name']: True for row in cursor.fetchall()}
except mariadb.Error as e:
print(f" ✗ Error checking procedures: {e}")
available_procedures = {}
# Test each procedure
procedure_tests = [
('select_base1_citizens', 'Citizens'),
('select_base1_monsters', 'Monsters'),
('select_base2_citizens', 'Citizens'),
('select_base2_monsters', 'Monsters'),
('select_random_domains', 'Domains'),
('select_random_dukes', 'Dukes')
]
for proc_name, card_type in procedure_tests:
if proc_name not in available_procedures:
print(f"\n{proc_name} - Procedure not found (skipping)")
continue
try:
print(f"\n Testing {proc_name}():")
cursor.callproc(proc_name)
results = cursor.fetchall()
if not results:
print(f" ⚠ No results returned")
continue
print(f" ✓ Returned {len(results)} {card_type.lower()}")
# Display results based on card type (using dictionary access)
if card_type == 'Citizens':
print(f" {card_type} returned:")
for row in results:
name = row.get('name', 'N/A')
gc = row.get('gold_cost')
r1 = row.get('roll_match1')
r2 = row.get('roll_match2')
sh = row.get('shadow_count', 0) or 0
ho = row.get('holy_count', 0) or 0
so = row.get('soldier_count', 0) or 0
wo = row.get('worker_count', 0) or 0
exp = row.get('expansion')
roles = []
if sh: roles.append(f"{sh} Shadow")
if ho: roles.append(f"{ho} Holy")
if so: roles.append(f"{so} Soldier")
if wo: roles.append(f"{wo} Worker")
role_str = ", ".join(roles) if roles else "No roles"
roll_str = f"{r1}" + (f"/{r2}" if r2 and r2 > 0 else "")
gc_str = f"{gc}gp" if gc is not None else "N/A"
exp_str = f" | {exp}" if exp else ""
print(f" {name:20} | Cost: {gc_str:5} | Roll: {roll_str:5} | {role_str:20}{exp_str}")
elif card_type == 'Monsters':
print(f" {card_type} returned:")
for row in results:
name = row.get('name', 'N/A')
area = row.get('area')
mtype = row.get('monster_type')
sc = row.get('strength_cost', 0) or 0
mc = row.get('magic_cost', 0) or 0
vp = row.get('vp_reward', 0) or 0
gr = row.get('gold_reward', 0) or 0
exp = row.get('expansion')
cost_str = f"{sc}sp" + (f" + {mc}mp" if mc else "")
reward_str = f"{vp}vp" + (f" + {gr}gp" if gr else "")
exp_str = f" | {exp}" if exp else ""
print(f" {name:25} | {area:10} | {mtype:8} | Cost: {cost_str:10} | Reward: {reward_str:15}{exp_str}")
elif card_type == 'Domains':
print(f" {card_type} returned:")
for row in results:
name = row.get('name', 'N/A')
gc = row.get('gold_cost')
vp = row.get('vp_reward')
text = row.get('text')
text_preview = (text[:50] + "...") if text and len(text) > 50 else (text or "No effect")
print(f" {name:25} | Cost: {gc:2}gp | {vp}vp | {text_preview}")
elif card_type == 'Dukes':
print(f" {card_type} returned:")
for row in results:
name = row.get('name', 'N/A')
gm = row.get('gold_mult', 0) or 0
sm = row.get('strength_mult', 0) or 0
mm = row.get('magic_mult', 0) or 0
mults = []
if gm: mults.append(f"Gold×{gm}")
if sm: mults.append(f"Str×{sm}")
if mm: mults.append(f"Mag×{mm}")
mult_str = ", ".join(mults) if mults else "No multipliers"
print(f" {name:30} | {mult_str}")
except mariadb.Error as e:
print(f" ✗ Error calling {proc_name}: {e}")
# Cleanup
cursor.close()
connection.close()
print("\n" + "=" * 50)
print("Database test completed")
return True
if __name__ == "__main__":
success = test_database_connection()
sys.exit(0 if success else 1)