golfgame/scripts/install.sh
adlee-was-taken 9fc6b83bba v3.0.0: V3 features, server refactoring, and documentation overhaul
- 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>
2026-02-14 10:03:45 -05:00

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