- Extract WebSocket handlers from main.py into handlers.py - Add V3 feature docs (dealer rotation, dealing animation, round end reveal, column pair celebration, final turn urgency, opponent thinking, score tallying, card hover/selection, knock early drama, column pair indicator, swap animation improvements, draw source distinction, card value tooltips, active rules context, discard pile history, realistic card sounds) - Add V3 refactoring docs (ai.py, main.py/game.py, misc improvements) - Add installation guide with Docker, systemd, and nginx setup - Add helper scripts (install.sh, dev-server.sh, docker-build.sh) - Add animation flow diagrams documentation - Add test files for handlers, rooms, and V3 features - Add e2e test specs for V3 features - Update README with complete project structure and current tech stack - Update CLAUDE.md with full architecture tree and server layer descriptions - Update .env.example to reflect PostgreSQL (remove SQLite references) - Update .gitignore to exclude virtualenv files, .claude/, and .db files - Remove tracked virtualenv files (bin/, lib64, pyvenv.cfg) - Remove obsolete game_log.py (SQLite) and games.db Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
530 lines
14 KiB
Bash
Executable File
530 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Golf Game Installer
|
|
#
|
|
# This script provides a menu-driven installation for the Golf card game.
|
|
# Run with: ./scripts/install.sh
|
|
#
|
|
set -e
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Get the directory where this script lives
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
|
|
echo -e "${BLUE}"
|
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
|
echo "║ Golf Game Installer ║"
|
|
echo "╚══════════════════════════════════════════════════════════════╝"
|
|
echo -e "${NC}"
|
|
|
|
show_menu() {
|
|
echo ""
|
|
echo "Select an option:"
|
|
echo ""
|
|
echo " 1) Development Setup"
|
|
echo " - Start Docker services (PostgreSQL, Redis)"
|
|
echo " - Create Python virtual environment"
|
|
echo " - Install dependencies"
|
|
echo " - Create .env from template"
|
|
echo ""
|
|
echo " 2) Production Install to /opt/golfgame"
|
|
echo " - Install application to /opt/golfgame"
|
|
echo " - Create production .env"
|
|
echo " - Set up systemd service"
|
|
echo ""
|
|
echo " 3) Docker Services Only"
|
|
echo " - Start PostgreSQL and Redis containers"
|
|
echo ""
|
|
echo " 4) Create/Update Systemd Service"
|
|
echo " - Create or update the systemd service file"
|
|
echo ""
|
|
echo " 5) Uninstall Production"
|
|
echo " - Stop and remove systemd service"
|
|
echo " - Optionally remove /opt/golfgame"
|
|
echo ""
|
|
echo " 6) Show Status"
|
|
echo " - Check Docker containers"
|
|
echo " - Check systemd service"
|
|
echo " - Test endpoints"
|
|
echo ""
|
|
echo " q) Quit"
|
|
echo ""
|
|
}
|
|
|
|
check_requirements() {
|
|
local missing=()
|
|
|
|
if ! command -v python3 &> /dev/null; then
|
|
missing+=("python3")
|
|
fi
|
|
|
|
if ! command -v docker &> /dev/null; then
|
|
missing+=("docker")
|
|
fi
|
|
|
|
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
|
missing+=("docker-compose")
|
|
fi
|
|
|
|
if [ ${#missing[@]} -gt 0 ]; then
|
|
echo -e "${RED}Missing required tools: ${missing[*]}${NC}"
|
|
echo "Please install them before continuing."
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
start_docker_services() {
|
|
echo -e "${BLUE}Starting Docker services...${NC}"
|
|
cd "$PROJECT_DIR"
|
|
|
|
if docker compose version &> /dev/null; then
|
|
docker compose -f docker-compose.dev.yml up -d
|
|
else
|
|
docker-compose -f docker-compose.dev.yml up -d
|
|
fi
|
|
|
|
echo -e "${GREEN}Docker services started.${NC}"
|
|
echo ""
|
|
echo "Services:"
|
|
echo " - PostgreSQL: localhost:5432 (user: golf, password: devpassword, db: golf)"
|
|
echo " - Redis: localhost:6379"
|
|
}
|
|
|
|
setup_dev_venv() {
|
|
echo -e "${BLUE}Setting up Python virtual environment...${NC}"
|
|
cd "$PROJECT_DIR"
|
|
|
|
# Check Python version
|
|
PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
|
echo "Using Python $PYTHON_VERSION"
|
|
|
|
# Remove old venv if it exists and is broken
|
|
if [ -f "pyvenv.cfg" ]; then
|
|
if [ -L "bin/python" ] && [ ! -e "bin/python" ]; then
|
|
echo -e "${YELLOW}Removing broken virtual environment...${NC}"
|
|
rm -rf bin lib lib64 pyvenv.cfg include share 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
# Create venv if it doesn't exist
|
|
if [ ! -f "pyvenv.cfg" ]; then
|
|
echo "Creating virtual environment..."
|
|
python3 -m venv .
|
|
fi
|
|
|
|
# Install dependencies
|
|
echo "Installing dependencies..."
|
|
./bin/pip install --upgrade pip
|
|
./bin/pip install -e ".[dev]"
|
|
|
|
echo -e "${GREEN}Virtual environment ready.${NC}"
|
|
}
|
|
|
|
setup_dev_env() {
|
|
echo -e "${BLUE}Setting up .env file...${NC}"
|
|
cd "$PROJECT_DIR"
|
|
|
|
if [ -f ".env" ]; then
|
|
echo -e "${YELLOW}.env file already exists. Overwrite? (y/N)${NC}"
|
|
read -r response
|
|
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
|
echo "Keeping existing .env"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
cat > .env << 'EOF'
|
|
# Golf Game Development Configuration
|
|
# Generated by install.sh
|
|
|
|
HOST=0.0.0.0
|
|
PORT=8000
|
|
DEBUG=true
|
|
LOG_LEVEL=DEBUG
|
|
ENVIRONMENT=development
|
|
|
|
# PostgreSQL (from docker-compose.dev.yml)
|
|
DATABASE_URL=postgresql://golf:devpassword@localhost:5432/golf
|
|
POSTGRES_URL=postgresql://golf:devpassword@localhost:5432/golf
|
|
|
|
# Room Settings
|
|
MAX_PLAYERS_PER_ROOM=6
|
|
ROOM_TIMEOUT_MINUTES=60
|
|
ROOM_CODE_LENGTH=4
|
|
|
|
# Game Defaults
|
|
DEFAULT_ROUNDS=9
|
|
DEFAULT_INITIAL_FLIPS=2
|
|
DEFAULT_USE_JOKERS=false
|
|
DEFAULT_FLIP_ON_DISCARD=false
|
|
EOF
|
|
|
|
echo -e "${GREEN}.env file created.${NC}"
|
|
}
|
|
|
|
dev_setup() {
|
|
echo -e "${BLUE}=== Development Setup ===${NC}"
|
|
echo ""
|
|
|
|
if ! check_requirements; then
|
|
return 1
|
|
fi
|
|
|
|
start_docker_services
|
|
echo ""
|
|
|
|
setup_dev_venv
|
|
echo ""
|
|
|
|
setup_dev_env
|
|
echo ""
|
|
|
|
echo -e "${GREEN}=== Development Setup Complete ===${NC}"
|
|
echo ""
|
|
echo "To start the development server:"
|
|
echo ""
|
|
echo " cd $PROJECT_DIR/server"
|
|
echo " ../bin/uvicorn main:app --reload --host 0.0.0.0 --port 8000"
|
|
echo ""
|
|
echo "Or use the helper script:"
|
|
echo ""
|
|
echo " $PROJECT_DIR/scripts/dev-server.sh"
|
|
echo ""
|
|
}
|
|
|
|
prod_install() {
|
|
echo -e "${BLUE}=== Production Installation ===${NC}"
|
|
echo ""
|
|
|
|
INSTALL_DIR="/opt/golfgame"
|
|
|
|
# Check if running as root or with sudo available
|
|
if [ "$EUID" -ne 0 ]; then
|
|
if ! command -v sudo &> /dev/null; then
|
|
echo -e "${RED}This option requires root privileges. Run with sudo or as root.${NC}"
|
|
return 1
|
|
fi
|
|
SUDO="sudo"
|
|
else
|
|
SUDO=""
|
|
fi
|
|
|
|
echo "This will install Golf Game to $INSTALL_DIR"
|
|
echo -e "${YELLOW}Continue? (y/N)${NC}"
|
|
read -r response
|
|
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
|
echo "Aborted."
|
|
return
|
|
fi
|
|
|
|
# Create directory
|
|
echo "Creating $INSTALL_DIR..."
|
|
$SUDO mkdir -p "$INSTALL_DIR"
|
|
|
|
# Copy files
|
|
echo "Copying application files..."
|
|
$SUDO cp -r "$PROJECT_DIR/server" "$INSTALL_DIR/"
|
|
$SUDO cp -r "$PROJECT_DIR/client" "$INSTALL_DIR/"
|
|
$SUDO cp "$PROJECT_DIR/pyproject.toml" "$INSTALL_DIR/"
|
|
$SUDO cp "$PROJECT_DIR/README.md" "$INSTALL_DIR/"
|
|
$SUDO cp "$PROJECT_DIR/INSTALL.md" "$INSTALL_DIR/"
|
|
$SUDO cp "$PROJECT_DIR/.env.example" "$INSTALL_DIR/"
|
|
$SUDO cp -r "$PROJECT_DIR/scripts" "$INSTALL_DIR/"
|
|
|
|
# Create venv
|
|
echo "Creating virtual environment..."
|
|
$SUDO python3 -m venv "$INSTALL_DIR"
|
|
$SUDO "$INSTALL_DIR/bin/pip" install --upgrade pip
|
|
$SUDO "$INSTALL_DIR/bin/pip" install "$INSTALL_DIR"
|
|
|
|
# Create production .env if it doesn't exist
|
|
if [ ! -f "$INSTALL_DIR/.env" ]; then
|
|
echo "Creating production .env..."
|
|
|
|
# Generate a secret key
|
|
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
|
|
|
$SUDO tee "$INSTALL_DIR/.env" > /dev/null << EOF
|
|
# Golf Game Production Configuration
|
|
# Generated by install.sh
|
|
|
|
HOST=0.0.0.0
|
|
PORT=8000
|
|
DEBUG=false
|
|
LOG_LEVEL=INFO
|
|
ENVIRONMENT=production
|
|
|
|
# PostgreSQL - UPDATE THESE VALUES
|
|
DATABASE_URL=postgresql://golf:CHANGE_ME@localhost:5432/golf
|
|
POSTGRES_URL=postgresql://golf:CHANGE_ME@localhost:5432/golf
|
|
|
|
# Security
|
|
SECRET_KEY=$SECRET_KEY
|
|
|
|
# Room Settings
|
|
MAX_PLAYERS_PER_ROOM=6
|
|
ROOM_TIMEOUT_MINUTES=60
|
|
ROOM_CODE_LENGTH=4
|
|
|
|
# Game Defaults
|
|
DEFAULT_ROUNDS=9
|
|
DEFAULT_INITIAL_FLIPS=2
|
|
DEFAULT_USE_JOKERS=false
|
|
DEFAULT_FLIP_ON_DISCARD=false
|
|
|
|
# Optional: Sentry error tracking
|
|
# SENTRY_DSN=https://your-sentry-dsn
|
|
EOF
|
|
$SUDO chmod 600 "$INSTALL_DIR/.env"
|
|
fi
|
|
|
|
# Set ownership
|
|
echo "Setting permissions..."
|
|
$SUDO chown -R www-data:www-data "$INSTALL_DIR"
|
|
|
|
echo -e "${GREEN}Application installed to $INSTALL_DIR${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW}IMPORTANT: Edit $INSTALL_DIR/.env and update:${NC}"
|
|
echo " - DATABASE_URL / POSTGRES_URL with your PostgreSQL credentials"
|
|
echo " - Any other settings as needed"
|
|
echo ""
|
|
|
|
# Offer to set up systemd
|
|
echo "Set up systemd service now? (Y/n)"
|
|
read -r response
|
|
if [[ ! "$response" =~ ^[Nn]$ ]]; then
|
|
setup_systemd
|
|
fi
|
|
}
|
|
|
|
setup_systemd() {
|
|
echo -e "${BLUE}=== Systemd Service Setup ===${NC}"
|
|
echo ""
|
|
|
|
INSTALL_DIR="/opt/golfgame"
|
|
SERVICE_FILE="/etc/systemd/system/golfgame.service"
|
|
|
|
if [ "$EUID" -ne 0 ]; then
|
|
if ! command -v sudo &> /dev/null; then
|
|
echo -e "${RED}This option requires root privileges.${NC}"
|
|
return 1
|
|
fi
|
|
SUDO="sudo"
|
|
else
|
|
SUDO=""
|
|
fi
|
|
|
|
if [ ! -d "$INSTALL_DIR" ]; then
|
|
echo -e "${RED}$INSTALL_DIR does not exist. Run production install first.${NC}"
|
|
return 1
|
|
fi
|
|
|
|
echo "Creating systemd service..."
|
|
|
|
$SUDO tee "$SERVICE_FILE" > /dev/null << 'EOF'
|
|
[Unit]
|
|
Description=Golf Card Game Server
|
|
Documentation=https://github.com/alee/golfgame
|
|
After=network.target postgresql.service redis.service
|
|
Wants=postgresql.service redis.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=www-data
|
|
Group=www-data
|
|
WorkingDirectory=/opt/golfgame/server
|
|
Environment="PATH=/opt/golfgame/bin:/usr/local/bin:/usr/bin:/bin"
|
|
EnvironmentFile=/opt/golfgame/.env
|
|
ExecStart=/opt/golfgame/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
|
|
ExecReload=/bin/kill -HUP $MAINPID
|
|
Restart=always
|
|
RestartSec=5
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
|
|
# Security hardening
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
ReadWritePaths=/opt/golfgame
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
echo "Reloading systemd..."
|
|
$SUDO systemctl daemon-reload
|
|
|
|
echo "Enabling service..."
|
|
$SUDO systemctl enable golfgame
|
|
|
|
echo -e "${GREEN}Systemd service created.${NC}"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " sudo systemctl start golfgame # Start the service"
|
|
echo " sudo systemctl stop golfgame # Stop the service"
|
|
echo " sudo systemctl restart golfgame # Restart the service"
|
|
echo " sudo systemctl status golfgame # Check status"
|
|
echo " journalctl -u golfgame -f # View logs"
|
|
echo ""
|
|
|
|
echo "Start the service now? (Y/n)"
|
|
read -r response
|
|
if [[ ! "$response" =~ ^[Nn]$ ]]; then
|
|
$SUDO systemctl start golfgame
|
|
sleep 2
|
|
$SUDO systemctl status golfgame --no-pager
|
|
fi
|
|
}
|
|
|
|
uninstall_prod() {
|
|
echo -e "${BLUE}=== Production Uninstall ===${NC}"
|
|
echo ""
|
|
|
|
if [ "$EUID" -ne 0 ]; then
|
|
if ! command -v sudo &> /dev/null; then
|
|
echo -e "${RED}This option requires root privileges.${NC}"
|
|
return 1
|
|
fi
|
|
SUDO="sudo"
|
|
else
|
|
SUDO=""
|
|
fi
|
|
|
|
echo -e "${YELLOW}This will stop and remove the systemd service.${NC}"
|
|
echo "Continue? (y/N)"
|
|
read -r response
|
|
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
|
echo "Aborted."
|
|
return
|
|
fi
|
|
|
|
# Stop and disable service
|
|
if [ -f "/etc/systemd/system/golfgame.service" ]; then
|
|
echo "Stopping service..."
|
|
$SUDO systemctl stop golfgame 2>/dev/null || true
|
|
$SUDO systemctl disable golfgame 2>/dev/null || true
|
|
$SUDO rm -f /etc/systemd/system/golfgame.service
|
|
$SUDO systemctl daemon-reload
|
|
echo "Service removed."
|
|
else
|
|
echo "No systemd service found."
|
|
fi
|
|
|
|
# Optionally remove installation directory
|
|
if [ -d "/opt/golfgame" ]; then
|
|
echo ""
|
|
echo -e "${YELLOW}Remove /opt/golfgame directory? (y/N)${NC}"
|
|
read -r response
|
|
if [[ "$response" =~ ^[Yy]$ ]]; then
|
|
$SUDO rm -rf /opt/golfgame
|
|
echo "Directory removed."
|
|
else
|
|
echo "Directory kept."
|
|
fi
|
|
fi
|
|
|
|
echo -e "${GREEN}Uninstall complete.${NC}"
|
|
}
|
|
|
|
show_status() {
|
|
echo -e "${BLUE}=== Status Check ===${NC}"
|
|
echo ""
|
|
|
|
# Docker containers
|
|
echo "Docker Containers:"
|
|
if command -v docker &> /dev/null; then
|
|
docker ps --filter "name=golfgame" --format " {{.Names}}: {{.Status}}" 2>/dev/null || echo " (none running)"
|
|
echo ""
|
|
else
|
|
echo " Docker not installed"
|
|
echo ""
|
|
fi
|
|
|
|
# Systemd service
|
|
echo "Systemd Service:"
|
|
if [ -f "/etc/systemd/system/golfgame.service" ]; then
|
|
systemctl status golfgame --no-pager 2>/dev/null | head -5 || echo " Service not running"
|
|
else
|
|
echo " Not installed"
|
|
fi
|
|
echo ""
|
|
|
|
# Health check
|
|
echo "Health Check:"
|
|
for port in 8000; do
|
|
if curl -s "http://localhost:$port/health" > /dev/null 2>&1; then
|
|
response=$(curl -s "http://localhost:$port/health")
|
|
echo -e " Port $port: ${GREEN}OK${NC} - $response"
|
|
else
|
|
echo -e " Port $port: ${RED}Not responding${NC}"
|
|
fi
|
|
done
|
|
echo ""
|
|
|
|
# Database
|
|
echo "PostgreSQL:"
|
|
if command -v pg_isready &> /dev/null; then
|
|
if pg_isready -h localhost -p 5432 > /dev/null 2>&1; then
|
|
echo -e " ${GREEN}Running${NC} on localhost:5432"
|
|
else
|
|
echo -e " ${RED}Not responding${NC}"
|
|
fi
|
|
else
|
|
if docker ps --filter "name=postgres" --format "{{.Names}}" 2>/dev/null | grep -q postgres; then
|
|
echo -e " ${GREEN}Running${NC} (Docker)"
|
|
else
|
|
echo " Unable to check (pg_isready not installed)"
|
|
fi
|
|
fi
|
|
echo ""
|
|
|
|
# Redis
|
|
echo "Redis:"
|
|
if command -v redis-cli &> /dev/null; then
|
|
if redis-cli ping > /dev/null 2>&1; then
|
|
echo -e " ${GREEN}Running${NC} on localhost:6379"
|
|
else
|
|
echo -e " ${RED}Not responding${NC}"
|
|
fi
|
|
else
|
|
if docker ps --filter "name=redis" --format "{{.Names}}" 2>/dev/null | grep -q redis; then
|
|
echo -e " ${GREEN}Running${NC} (Docker)"
|
|
else
|
|
echo " Unable to check (redis-cli not installed)"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Main loop
|
|
while true; do
|
|
show_menu
|
|
echo -n "Enter choice: "
|
|
read -r choice
|
|
|
|
case $choice in
|
|
1) dev_setup ;;
|
|
2) prod_install ;;
|
|
3) start_docker_services ;;
|
|
4) setup_systemd ;;
|
|
5) uninstall_prod ;;
|
|
6) show_status ;;
|
|
q|Q) echo "Goodbye!"; exit 0 ;;
|
|
*) echo -e "${RED}Invalid option${NC}" ;;
|
|
esac
|
|
|
|
echo ""
|
|
echo "Press Enter to continue..."
|
|
read -r
|
|
done
|