expanded basic game features
This commit is contained in:
18
activate_with_env.sh
Executable file
18
activate_with_env.sh
Executable 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!"
|
||||
|
||||
55
cards.py
55
cards.py
@@ -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:
|
||||
def __init__(self):
|
||||
self.name = ""
|
||||
@@ -81,10 +92,10 @@ class Citizen(Card):
|
||||
self.gold_cost = gold_cost
|
||||
self.roll_match1 = roll_match1
|
||||
self.roll_match2 = roll_match2
|
||||
self.shadow_count = shadow_count
|
||||
self.holy_count = holy_count
|
||||
self.soldier_count = soldier_count
|
||||
self.worker_count = worker_count
|
||||
self.shadow_count = _coerce_int(shadow_count)
|
||||
self.holy_count = _coerce_int(holy_count)
|
||||
self.soldier_count = _coerce_int(soldier_count)
|
||||
self.worker_count = _coerce_int(worker_count)
|
||||
self.gold_payout_on_turn = gold_payout_on_turn
|
||||
self.gold_payout_off_turn = gold_payout_off_turn
|
||||
self.strength_payout_on_turn = strength_payout_on_turn
|
||||
@@ -112,6 +123,12 @@ class Citizen(Card):
|
||||
"holy_count": self.holy_count,
|
||||
"soldier_count": self.soldier_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_off_turn": self.gold_payout_off_turn,
|
||||
"strength_payout_on_turn": self.strength_payout_on_turn,
|
||||
@@ -132,10 +149,10 @@ class Citizen(Card):
|
||||
gold_cost=dict_["gold_cost"],
|
||||
roll_match1=dict_["roll_match1"],
|
||||
roll_match2=dict_["roll_match2"],
|
||||
shadow_count=dict_["shadow_count"],
|
||||
holy_count=dict_["holy_count"],
|
||||
soldier_count=dict_["soldier_count"],
|
||||
worker_count=dict_["worker_count"],
|
||||
shadow_count=dict_.get("shadow_count"),
|
||||
holy_count=dict_.get("holy_count"),
|
||||
soldier_count=dict_.get("soldier_count"),
|
||||
worker_count=dict_.get("worker_count"),
|
||||
gold_payout_on_turn=dict_["gold_payout_on_turn"],
|
||||
gold_payout_off_turn=dict_["gold_payout_off_turn"],
|
||||
strength_payout_on_turn=dict_["strength_payout_on_turn"],
|
||||
@@ -157,10 +174,10 @@ class Domain(Card):
|
||||
self.domain_id = domain_id
|
||||
self.name = name
|
||||
self.gold_cost = gold_cost
|
||||
self.shadow_count = shadow_count
|
||||
self.holy_count = holy_count
|
||||
self.soldier_count = soldier_count
|
||||
self.worker_count = worker_count
|
||||
self.shadow_count = _coerce_int(shadow_count)
|
||||
self.holy_count = _coerce_int(holy_count)
|
||||
self.soldier_count = _coerce_int(soldier_count)
|
||||
self.worker_count = _coerce_int(worker_count)
|
||||
self.vp_reward = vp_reward
|
||||
self.has_activation_effect = has_activation_effect
|
||||
self.has_passive_effect = has_passive_effect
|
||||
@@ -179,6 +196,12 @@ class Domain(Card):
|
||||
"holy_count": self.holy_count,
|
||||
"soldier_count": self.soldier_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,
|
||||
"has_activation_effect": self.has_activation_effect,
|
||||
"has_passive_effect": self.has_passive_effect,
|
||||
@@ -194,10 +217,10 @@ class Domain(Card):
|
||||
domain_id=dict_['domain_id'],
|
||||
name=dict_['name'],
|
||||
gold_cost=dict_['gold_cost'],
|
||||
shadow_count=dict_['shadow_count'],
|
||||
holy_count=dict_['holy_count'],
|
||||
soldier_count=dict_['soldier_count'],
|
||||
worker_count=dict_['worker_count'],
|
||||
shadow_count=dict_.get('shadow_count'),
|
||||
holy_count=dict_.get('holy_count'),
|
||||
soldier_count=dict_.get('soldier_count'),
|
||||
worker_count=dict_.get('worker_count'),
|
||||
vp_reward=dict_['vp_reward'],
|
||||
has_activation_effect=dict_['has_activation_effect'],
|
||||
has_passive_effect=dict_['has_passive_effect'],
|
||||
|
||||
40
check_db_server.py
Normal file
40
check_db_server.py
Normal 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
13
docs/README.md
Normal 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
79
docs/README_SERVER.md
Normal 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
64
docs/database.md
Normal 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
33
docs/dev-setup.md
Normal 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
121
docs/game.md
Normal 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
88
docs/server.md
Normal 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
31
docs/testing.md
Normal 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 `/`.
|
||||
|
||||
22
requirements.txt
Normal file
22
requirements.txt
Normal 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
|
||||
|
||||
40
setup_venv.sh
Executable file
40
setup_venv.sh
Executable 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
107
sql/INSTALL_PROCEDURES.md
Normal 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
129
sql/USER_SETUP_GUIDE.md
Normal 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';
|
||||
```
|
||||
|
||||
@@ -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
|
||||
('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),
|
||||
('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),
|
||||
|
||||
76
sql/create_all_stored_procedures.sql
Normal file
76
sql/create_all_stored_procedures.sql
Normal 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
56
sql/fix_user_setup.sql
Normal 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
63
sql/run_sql.sh
Executable 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
359
test_database.py
Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user