diff --git a/docker-compose.all.yml b/docker-compose.all.yml index 905cc9f..4c30dd8 100644 --- a/docker-compose.all.yml +++ b/docker-compose.all.yml @@ -1,5 +1,5 @@ -# 全量服务部署 (后端 + 游戏服 + 数据库) -# 使用方法: docker-compose -f docker-compose.all.yml up -d +# 全量服务部署 (前端 + 后端 + 游戏服 + 数据库 + 中间件 + Nginx) +# 使用方法: docker-compose -f docker-compose.all.yml up -d --build services: # ---------------------------------------------------- # 1. 业务后端 (Bindbox Game Backend) @@ -8,11 +8,11 @@ services: image: zfc931912343/bindbox-game:v1.15 container_name: bindbox-game restart: always - ports: - - "9991:9991" + # ports: + # - "9991:9991" (Internal only) volumes: - ../bindbox_game/logs:/app/logs - # - ../bindbox_game/configs:/app/configs # 指向 bindbox_game 目录下的配置 + - ../bindbox_game/configs:/app/configs environment: - ACTIVE_ENV=pro - TZ=Asia/Shanghai @@ -23,8 +23,27 @@ services: options: max-size: "10m" max-file: "3" + depends_on: + - mysql + - redis + # ---------------------------------------------------- - # 2. 游戏数据库 (CockroachDB for Nakama) + # 2. 管理后台 (Admin Web) + # ---------------------------------------------------- + admin-web: + build: ../bindbox_game/web/admin + container_name: bindbox-admin-web + restart: always + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 3. 游戏数据库 (CockroachDB for Nakama) # ---------------------------------------------------- nakama-db: image: cockroachdb/cockroach:latest-v23.1 @@ -33,9 +52,7 @@ services: restart: always volumes: - nakama-db-data:/var/lib/cockroach - ports: - - "26257:26257" - - "8081:8080" + healthcheck: test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ] interval: 3s @@ -50,14 +67,14 @@ services: options: max-size: "10m" max-file: "3" + # ---------------------------------------------------- - # 3. 游戏服务器 (Nakama) + # 4. 游戏服务器 (Nakama) # ---------------------------------------------------- nakama: image: zfc931912343/bindbox-saolei:v1.6 container_name: nakama-server environment: - # 直接使用服务名访问后端 - MINESWEEPER_BACKEND_URL=http://bindbox-game:9991/api/internal - TZ=Asia/Shanghai entrypoint: @@ -72,10 +89,7 @@ services: condition: service_started volumes: - nakama-data:/nakama/data - ports: - - "7350:7350" - - "7351:7351" - - "9100:9100" + healthcheck: test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://bindbox-game:9991/" ] interval: 10s @@ -88,9 +102,147 @@ services: options: max-size: "10m" max-file: "3" + + # ---------------------------------------------------- + # 5. MySQL Database (For Bindbox Backend) + # ---------------------------------------------------- + mysql: + image: mysql:8.0 + container_name: bindbox-mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: "123456" + MYSQL_DATABASE: "bindbox_game" + TZ: Asia/Shanghai + command: --default-authentication-plugin=mysql_native_password + volumes: + - mysql_data:/var/lib/mysql + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 6. Redis (For Bindbox Backend) + # ---------------------------------------------------- + redis: + image: redis:7.0 + container_name: bindbox-redis + restart: always + volumes: + - redis_data:/data + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 7. Nginx Gateway + # ---------------------------------------------------- + nginx: + image: nginx:latest + container_name: bindbox-nginx + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d + - ./nginx/ssl:/etc/nginx/ssl + depends_on: + - bindbox-game + - admin-web + - nakama + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + + # ---------------------------------------------------- + # 8. Loki (Log Storage) + # ---------------------------------------------------- + loki: + image: grafana/loki:3.0.0 + container_name: bindbox-loki + restart: always + # ports: + # - "3100:3100" + volumes: + - ./loki/loki-config.yaml:/etc/loki/local-config.yaml + - loki_data:/loki + command: -config.file=/etc/loki/local-config.yaml + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 9. Promtail (Log Collector) + # ---------------------------------------------------- + promtail: + image: grafana/promtail:3.0.0 + container_name: bindbox-promtail + restart: always + volumes: + - ./loki/promtail-config.yaml:/etc/promtail/config.yaml + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock + - ../bindbox_game/logs:/var/log/bindbox-game:ro + command: -config.file=/etc/promtail/config.yaml + networks: + - bindbox_net + depends_on: + - loki + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 10. Grafana (Visualization) + # ---------------------------------------------------- + grafana: + image: grafana/grafana:latest + container_name: bindbox-grafana + restart: always + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana_data:/var/lib/grafana + networks: + - bindbox_net + depends_on: + - loki + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + volumes: nakama-db-data: nakama-data: + mysql_data: + redis_data: + loki_data: + grafana_data: + + networks: bindbox_net: name: bindbox_net diff --git a/docker-compose.cloud.yml b/docker-compose.cloud.yml index 8f4a6ff..8764d49 100644 --- a/docker-compose.cloud.yml +++ b/docker-compose.cloud.yml @@ -1,55 +1,350 @@ -# 云端部署专用 - 扫雷游戏服务 -# 使用方法: docker-compose -f docker-compose.cloud.yml up -d +# 全量服务部署 (云端/无源码版) +# 使用方法: +# 1. 确保已将 docker-compose.cloud.yml, configs/, nginx/, loki/ 目录上传到服务器同一目录 +# 2. 确保 logs/ 目录存在 (mkdir logs) +# 3. 运行: docker-compose -f docker-compose.cloud.yml up -d services: + # ---------------------------------------------------- + # 1. 业务后端 (Bindbox Game Backend) + # ---------------------------------------------------- + bindbox-game: + image: zfc931912343/bindbox-game:v1.15 + container_name: bindbox-game + restart: always + # ports: + # - "9991:9991" (Internal only) + volumes: + # 改为挂载当前目录下的 logs 和 configs + - ./logs:/app/logs + - ./configs:/app/configs + environment: + - ACTIVE_ENV=pro + - TZ=Asia/Shanghai + # MySQL 配置(覆盖编译时的默认值) + - MYSQL_ADDR=mysql:3306 + - MYSQL_USER=root + - MYSQL_PASS=bindbox2025kdy + - MYSQL_NAME=bindbox_game + # Redis 配置 + - REDIS_ADDR=redis:6379 + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + depends_on: + - mysql + - redis + + # ---------------------------------------------------- + # 2. 游戏数据库 (CockroachDB for Nakama) + # ---------------------------------------------------- nakama-db: image: cockroachdb/cockroach:latest-v23.1 container_name: nakama-db - command: start-single-node --insecure --store=attrs=ssd,path=/var/lib/cockroach/ + command: start-single-node --insecure --store=attrs=ssd,path=/var/lib/cockroach/ --cache=.25 --max-sql-memory=.25 restart: always volumes: - nakama-db-data:/var/lib/cockroach - ports: - - "26257:26257" - - "8081:8080" healthcheck: test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ] interval: 3s timeout: 3s retries: 5 + environment: + - TZ=Asia/Shanghai + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + # ---------------------------------------------------- + # 3. 游戏服务器 (Nakama) + # ---------------------------------------------------- nakama: - image: zfc931912343/bindbox-saolei:v1.3 + image: zfc931912343/bindbox-saolei:v1.6 container_name: nakama-server environment: - # 使用 Docker 内部网络访问后端服务 (需确保在同一网络下) - - MINESWEEPER_BACKEND_URL=http://blindbox-mms-api:9991/api/internal + - MINESWEEPER_BACKEND_URL=http://bindbox-game:9991/api/internal + - TZ=Asia/Shanghai entrypoint: - "/bin/sh" - "-ecx" - - "/nakama/nakama migrate up --database.address root@nakama-db:26257 && exec /nakama/nakama --name nakama1 --database.address root@nakama-db:26257 --logger.level DEBUG --session.token_expiry_sec 7200 --metrics.prometheus_port 9100 --runtime.path /nakama/modules" + - "/nakama/nakama migrate up --database.address root@nakama-db:26257 && exec /nakama/nakama --name nakama1 --database.address root@nakama-db:26257 --logger.level DEBUG --session.token_expiry_sec 7200 --metrics.prometheus_port 9100 --runtime.path /nakama/modules --matchmaker.interval_sec 1 --matchmaker.max_intervals 5" restart: always depends_on: nakama-db: condition: service_healthy + bindbox-game: + condition: service_started volumes: - nakama-data:/nakama/data - ports: - - "7350:7350" - - "7351:7351" - - "9100:9100" healthcheck: - test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7350/" ] + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://bindbox-game:9991/" ] interval: 10s timeout: 5s retries: 5 + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 4. MySQL Database + # ---------------------------------------------------- + mysql: + image: mysql:8.0 + container_name: bindbox-mysql + restart: always + ports: + - "3306:3306" # 临时开放外部访问,用完记得关闭! + environment: + MYSQL_ROOT_PASSWORD: "bindbox2025kdy" + MYSQL_DATABASE: "bindbox_game" + TZ: Asia/Shanghai + command: --default-authentication-plugin=mysql_native_password + volumes: + - mysql_data:/var/lib/mysql + - ./mysql/init:/docker-entrypoint-initdb.d # 初始化脚本 + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 5. Redis + # ---------------------------------------------------- + redis: + image: redis:7.0 + container_name: bindbox-redis + restart: always + volumes: + - redis_data:/data + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 6. Nginx Gateway (入口) + # ---------------------------------------------------- + nginx: + image: nginx:latest + container_name: bindbox-nginx + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d + - ./nginx/ssl:/etc/nginx/ssl + - ./dist:/usr/share/nginx/html/admin + depends_on: + - bindbox-game + - nakama + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 7. Loki (日志存储) + # ---------------------------------------------------- + loki: + image: grafana/loki:3.0.0 + container_name: bindbox-loki + restart: always + volumes: + # 必须上传 loki 目录到服务器 + - ./loki/loki-config.yaml:/etc/loki/local-config.yaml + - loki_data:/loki + command: -config.file=/etc/loki/local-config.yaml + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 8. Promtail (日志采集) + # ---------------------------------------------------- + promtail: + image: grafana/promtail:3.0.0 + container_name: bindbox-promtail + restart: always + volumes: + - ./loki/promtail-config.yaml:/etc/promtail/config.yaml + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock + # 采集当前目录下的 logs 文件夹 + - ./logs:/var/log/bindbox-game:ro + command: -config.file=/etc/promtail/config.yaml + networks: + - bindbox_net + depends_on: + - loki + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 9. Grafana (日志界面) + # ---------------------------------------------------- + grafana: + image: grafana/grafana:latest + container_name: bindbox-grafana + restart: always + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana_data:/var/lib/grafana + networks: + - bindbox_net + depends_on: + - loki + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 10. Prometheus (指标采集) + # ---------------------------------------------------- + prometheus: + image: prom/prometheus:latest + container_name: bindbox-prometheus + restart: always + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.enable-lifecycle' + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 11. Nginx Exporter (Nginx指标导出) + # ---------------------------------------------------- + nginx-exporter: + image: nginx/nginx-prometheus-exporter:latest + container_name: bindbox-nginx-exporter + restart: always + command: + - -nginx.scrape-uri=http://nginx:80/nginx_status + networks: + - bindbox_net + depends_on: + - nginx + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 12. Redis Exporter (Redis指标导出) + # ---------------------------------------------------- + redis-exporter: + image: oliver006/redis_exporter:latest + container_name: bindbox-redis-exporter + restart: always + environment: + - REDIS_ADDR=redis://redis:6379 + networks: + - bindbox_net + depends_on: + - redis + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 13. MySQL Exporter (MySQL指标导出) + # ---------------------------------------------------- + mysql-exporter: + image: prom/mysqld-exporter:latest + container_name: bindbox-mysql-exporter + restart: always + command: + - --config.my-cnf=/etc/.my.cnf + volumes: + - ./mysql/.my.cnf:/etc/.my.cnf:ro + networks: + - bindbox_net + depends_on: + - mysql + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # ---------------------------------------------------- + # 14. Tempo (链路追踪) + # ---------------------------------------------------- + tempo: + image: grafana/tempo:latest + container_name: bindbox-tempo + restart: always + command: [ "-config.file=/etc/tempo/tempo-config.yaml" ] + volumes: + - ./tempo/tempo-config.yaml:/etc/tempo/tempo-config.yaml + - tempo_data:/var/tempo + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" volumes: nakama-db-data: nakama-data: + mysql_data: + redis_data: + loki_data: + grafana_data: + prometheus_data: + tempo_data: + - # 必须加入后端服务所在的网络才能通过 service_name 访问 networks: - default: - name: ${DOCKER_NETWORK_NAME:-bindbox_default} - external: true + bindbox_net: + name: bindbox_net + driver: bridge diff --git a/loki/loki-config.yaml b/loki/loki-config.yaml new file mode 100644 index 0000000..11544f7 --- /dev/null +++ b/loki/loki-config.yaml @@ -0,0 +1,38 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 0 + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +ruler: + alertmanager_url: http://localhost:9093 + +# Limit settings to prevent issues with large logs +limits_config: + reject_old_samples: true + reject_old_samples_max_age: 168h + ingestion_rate_mb: 20 + ingestion_burst_size_mb: 30 + volume_enabled: true diff --git a/loki/promtail-config.yaml b/loki/promtail-config.yaml new file mode 100644 index 0000000..940f48a --- /dev/null +++ b/loki/promtail-config.yaml @@ -0,0 +1,36 @@ +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: +# 1. Scrape logs from Docker containers via socket (stdout/stderr) +- job_name: docker + docker_sd_configs: + - host: unix:///var/run/docker.sock + refresh_interval: 5s + # filters: + # - name: name + # values: ["bindbox-game"] # Optional: Filter specifically if desired, but user said "all monitor" + relabel_configs: + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_log_stream'] + target_label: 'logstream' + - source_labels: ['__meta_docker_container_label_logging_jobname'] + target_label: 'job' + +# 2. Scrape logs from mounted log files (application logs) +- job_name: file_logs + static_configs: + - targets: + - localhost + labels: + job: bindbox_app_logs + __path__: /var/log/bindbox-game/*.log diff --git a/mysql/.DS_Store b/mysql/.DS_Store new file mode 100644 index 0000000..2746f40 Binary files /dev/null and b/mysql/.DS_Store differ diff --git a/mysql/.my.cnf b/mysql/.my.cnf new file mode 100644 index 0000000..8428187 --- /dev/null +++ b/mysql/.my.cnf @@ -0,0 +1,5 @@ +[client] +user = exporter +password = exporter123 +host = mysql +port = 3306 diff --git a/mysql/init/01-create-exporter-user.sql b/mysql/init/01-create-exporter-user.sql new file mode 100644 index 0000000..9267628 --- /dev/null +++ b/mysql/init/01-create-exporter-user.sql @@ -0,0 +1,4 @@ +-- 创建 MySQL Exporter 监控账号 +CREATE USER IF NOT EXISTS 'exporter'@'%' IDENTIFIED BY 'exporter123'; +GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'%'; +FLUSH PRIVILEGES; diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf new file mode 100644 index 0000000..52c9d1a --- /dev/null +++ b/nginx/conf.d/default.conf @@ -0,0 +1,78 @@ +server { + listen 80; + server_name kdy.1024tool.vip; + + # Nginx 状态监控端点 (HTTP) + location /nginx_status { + stub_status on; + access_log off; + allow 172.0.0.0/8; + allow 192.168.0.0/16; # Docker bridge network + allow 127.0.0.1; + deny all; + } + + location / { + return 301 https://$host$request_uri; + } +} + +# HTTPS Server +server { + listen 443 ssl; + server_name kdy.1024tool.vip; + + # SSL Config + ssl_certificate /etc/nginx/ssl/kdy.1024tool.vip.pem; + ssl_certificate_key /etc/nginx/ssl/kdy.1024tool.vip.key; + + ssl_session_timeout 5m; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; + ssl_prefer_server_ciphers on; + + # 1. Admin Frontend (Static Dist) + location / { + root /usr/share/nginx/html/admin; + index index.html index.htm; + try_files $uri $uri/ /index.html; # SPA Support + } + + # 2. Backend API + location /api/ { + proxy_pass http://bindbox-game:9991/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 3. Nakama API (HTTP / GRPC / WebSocket) + location /v2/ { + proxy_pass http://nakama:7350/v2/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Nakama WebSocket + location /ws { + proxy_pass http://nakama:7350/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # Nginx 状态监控端点(仅内网访问) + location /nginx_status { + stub_status on; + access_log off; + allow 172.0.0.0/8; # Docker 内网 + allow 10.0.0.0/8; # 内网 + allow 127.0.0.1; + deny all; + } +} diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml new file mode 100644 index 0000000..6773314 --- /dev/null +++ b/prometheus/prometheus.yml @@ -0,0 +1,33 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + # Prometheus 自身监控 + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + # Nginx 监控 + - job_name: 'nginx' + static_configs: + - targets: ['nginx-exporter:9113'] + metrics_path: /metrics + + # Redis 监控 + - job_name: 'redis' + static_configs: + - targets: ['redis-exporter:9121'] + metrics_path: /metrics + + # MySQL 监控 + - job_name: 'mysql' + static_configs: + - targets: ['mysql-exporter:9104'] + metrics_path: /metrics + + # Nakama 游戏服务监控 (如果开启了 metrics) + - job_name: 'nakama' + static_configs: + - targets: ['nakama:9100'] + metrics_path: /metrics diff --git a/server/backend.so b/server/backend.so index 4fc94bf..f08d612 100644 Binary files a/server/backend.so and b/server/backend.so differ diff --git a/server/core/types.go b/server/core/types.go index 48b4d88..d6c66df 100644 --- a/server/core/types.go +++ b/server/core/types.go @@ -72,7 +72,8 @@ type GameState struct { TurnOrder []string `json:"turnOrder"` CurrentTurnIndex int `json:"currentTurnIndex"` Round int `json:"round"` - GlobalTurnCount int `json:"globalTurnCount"` // 总回合数(用于狗狗技能) + GlobalTurnCount int `json:"globalTurnCount"` // 总回合数(用于狗狗技能) + LastDeadPlayerID string `json:"lastDeadPlayerId"` // 最后一个死亡的玩家ID(用于平局结算) WinnerID string `json:"winnerId"` GameStarted bool `json:"gameStarted"` LastMoveTimestamp int64 `json:"lastMoveTimestamp"` // Unix时间戳(秒) diff --git a/server/handlers/rpc.go b/server/handlers/rpc.go index 5700dc3..8bca828 100644 --- a/server/handlers/rpc.go +++ b/server/handlers/rpc.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "encoding/json" - "time" "github.com/heroiclabs/nakama-common/runtime" ) @@ -74,67 +73,36 @@ func RpcFindMyMatch(ctx context.Context, logger runtime.Logger, db *sql.DB, nk r return readObjects[0].Value, nil } -// RpcGetOnlineCount 返回当前在线玩家数量(基于心跳) +// RpcGetOnlineCount 返回当前在线玩家数量 func RpcGetOnlineCount(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string) if !ok || userID == "" { return "", runtime.NewError("user not authenticated", 16) } - // 1. 更新当前用户的心跳时间戳 - now := time.Now().Unix() - heartbeatData, _ := json.Marshal(map[string]int64{"ts": now}) - - _, err := nk.StorageWrite(ctx, []*runtime.StorageWrite{ - { - Collection: "game_lobby", - Key: "heartbeat", - UserID: userID, - Value: string(heartbeatData), - PermissionRead: 0, // 不可读 - PermissionWrite: 0, // 仅服务器可写 - }, - }) + // 获取所有活跃比赛中的玩家数 + matches, err := nk.MatchList(ctx, 100, true, "", nil, nil, "*") if err != nil { - logger.Warn("Failed to write heartbeat: %v", err) + logger.Warn("Failed to list matches: %v", err) } - // 2. 读取所有用户的心跳记录 - cursor := "" - onlineCount := 0 - expireThreshold := now - 60 // 60秒内有心跳的算在线 - - for { - // StorageList(ctx, callerID, collection, userID, limit, cursor) - objects, nextCursor, err := nk.StorageList(ctx, "", "game_lobby", "", 100, cursor) - if err != nil { - logger.Error("Failed to list heartbeats: %v", err) - break - } - - for _, obj := range objects { - var data map[string]int64 - if err := json.Unmarshal([]byte(obj.Value), &data); err != nil { - continue - } - if ts, ok := data["ts"]; ok && ts >= expireThreshold { - onlineCount++ - } - } - - if nextCursor == "" { - break - } - cursor = nextCursor - } - - // 3. 获取比赛中的玩家数 - matches, _ := nk.MatchList(ctx, 100, true, "", nil, nil, "*") inGameCount := 0 for _, m := range matches { inGameCount += int(m.GetSize()) } + // 使用 Nakama 的 StreamCount 获取当前所有 WebSocket 连接数 + // Mode 0 = Notifications stream, 所有已认证的 WebSocket 连接都会加入这个流 + onlineCount, err := nk.StreamCount(0, "", "", "") + if err != nil { + logger.Warn("Failed to get stream count: %v", err) + onlineCount = inGameCount // 降级为比赛中玩家数 + } + + if onlineCount < 1 { + onlineCount = 1 // 至少自己在线 + } + result := map[string]interface{}{ "online_count": onlineCount, "match_count": len(matches), diff --git a/server/items/effects.go b/server/items/effects.go index 1be9cfb..473e2ce 100644 --- a/server/items/effects.go +++ b/server/items/effects.go @@ -55,6 +55,7 @@ func (s *PoisonStrategy) Use(state *core.GameState, user *core.Player, ctx ItemC }) } else { target.Poisoned = true + target.PoisonSteps = 0 ctx.Logic.BroadcastEvent(core.GameEvent{ Type: "item", PlayerID: user.UserID, PlayerName: user.Username, TargetID: target.UserID, TargetName: target.Username, ItemID: "poison", Message: fmt.Sprintf("☠️ %s 中毒了!", target.Username), @@ -80,14 +81,6 @@ func (s *ShieldStrategy) Use(state *core.GameState, user *core.Player, ctx ItemC type SkipStrategy struct{} func (s *SkipStrategy) Use(state *core.GameState, user *core.Player, ctx ItemContext) bool { - if user.Character == "elephant" { - ctx.Logger.Info("Elephant refused skip") - ctx.Logic.BroadcastEvent(core.GameEvent{ - Type: "ability", PlayerID: user.UserID, PlayerName: user.Username, ItemID: "skip", - Message: "🐘 大象无法使用该道具!", - }) - return false - } user.SkipTurn = true user.Shield = true ctx.Logic.BroadcastEvent(core.GameEvent{ diff --git a/server/items/items_test.go b/server/items/items_test.go index 2d7e928..d109960 100644 --- a/server/items/items_test.go +++ b/server/items/items_test.go @@ -65,6 +65,10 @@ func (m *MockGameLogic) BroadcastEvent(event core.GameEvent) { m.LastEvent = &event } +func (m *MockGameLogic) SendPrivateEvent(targetID string, event core.GameEvent) { + m.LastEvent = &event +} + // --- Helper --- func createTestContext(logic GameLogic) ItemContext { @@ -217,8 +221,11 @@ func TestSkip(t *testing.T) { user.Character = "elephant" user.SkipTurn = false consumed = strategy.Use(state, user, ctx) - if consumed { - t.Error("Elephant should refuse skip") + if !consumed { + t.Error("Elephant should now be able to use skip") + } + if !user.SkipTurn { + t.Error("Elephant skip turn should be true") } } diff --git a/server/logic/combat.go b/server/logic/combat.go index 1ca4c0b..8292e65 100644 --- a/server/logic/combat.go +++ b/server/logic/combat.go @@ -93,8 +93,9 @@ func (e *GameEngine) ApplyDamage(state *core.GameState, target *core.Player, amo } } - if target.HP < 0 { + if target.HP <= 0 { target.HP = 0 + state.LastDeadPlayerID = target.UserID } } diff --git a/server/logic/comprehensive_test.go b/server/logic/comprehensive_test.go index 920dcd3..04fb86d 100644 --- a/server/logic/comprehensive_test.go +++ b/server/logic/comprehensive_test.go @@ -226,11 +226,12 @@ func TestItem_Skip_Effect(t *testing.T) { }) } -func TestItem_Skip_ElephantCannotUse(t *testing.T) { +func TestItem_Skip_ElephantCanUse(t *testing.T) { RunScenario(t, GameScenario{ - Name: "好人卡-大象无法使用", + Name: "好人卡-大象现在可以使用", Players: []PlayerSetup{ {ID: "elephant", Character: "elephant", HP: 5}, + {ID: "p2", Character: "dog", HP: 4}, }, Grid: []CellSetup{ {Index: 0, Type: "item", ItemID: "skip"}, @@ -239,8 +240,8 @@ func TestItem_Skip_ElephantCannotUse(t *testing.T) { {Type: "move", PlayerID: "elephant", Value: 0}, }, Checks: []ScenarioCheck{ - {PlayerID: "elephant", Field: "skip_turn", Expected: false, Message: "大象无法使用好人卡"}, - {PlayerID: "elephant", Field: "shield", Expected: false, Message: "大象不应该获得护盾"}, + {PlayerID: "elephant", Field: "skip_turn", Expected: true, Message: "大象现在应该可以使用好人卡"}, + {PlayerID: "elephant", Field: "shield", Expected: true, Message: "大象现在应该获得护盾"}, }, }) } @@ -488,18 +489,18 @@ func TestCharacter_Dog_MagnifierAbility(t *testing.T) { } state.Grid[99].Type = "bomb" // 放一个未揭示的炸弹 - // 直接设置 GlobalTurnCount 为 5,下一次操作将使其变为 6 - state.GlobalTurnCount = 5 + // 狗狗独立步数设为 5,下一次操作将使其变为 6 + state.Players["dog"].DogStepCount = 5 - // 狗狗操作,GlobalTurnCount 变为 6,6 % 6 == 0,应该触发 + // 狗狗操作,DogStepCount 变为 6,6 % 6 == 0,应该触发 engine.HandleMove(state, "dog", 0) - t.Logf("操作后 GlobalTurnCount=%d, RevealedCells=%d", - state.GlobalTurnCount, len(state.Players["dog"].RevealedCells)) + t.Logf("操作后 DogStepCount=%d, RevealedCells=%d", + state.Players["dog"].DogStepCount, len(state.Players["dog"].RevealedCells)) // 检查是否触发了放大镜 if len(state.Players["dog"].RevealedCells) == 0 { - t.Errorf("狗狗应该在 GlobalTurnCount=6 时触发放大镜能力") + t.Errorf("狗狗应该在 DogStepCount=6 时触发放大镜能力") } else { t.Logf("狗狗放大镜触发成功,揭示了 %d 个格子", len(state.Players["dog"].RevealedCells)) } @@ -667,6 +668,30 @@ func TestGameFlow_GameOver(t *testing.T) { } } +func TestGameFlow_DrawLastManStanding(t *testing.T) { + engine, state := createScenarioState(GameScenario{ + Players: []PlayerSetup{ + {ID: "p1", Character: "dog", HP: 1}, + {ID: "p2", Character: "cat", HP: 1}, + }, + Grid: []CellSetup{ + {Index: 0, Type: "item", ItemID: "lightning"}, // 闪电对所有人造成1点伤害 + }, + }) + + // p1 使用闪电,所有人同时死亡 + // 按照逻辑,闪电会依次调用 ApplyDamage 给 p1, p2 + // p2 是最后一个被处理的(也是最后一个 HP 归零的),所以应该是 LastDeadPlayerID + engine.HandleMove(state, "p1", 0) + + if state.WinnerID != "p2" { + t.Errorf("平局时由于p2是处理列表最后一个死亡的,应该获胜。got winner=%s", state.WinnerID) + } + if state.GameStarted { + t.Error("游戏应该结束") + } +} + func TestGameFlow_SafeAreaExpansion(t *testing.T) { engine, state := createScenarioState(GameScenario{ Players: []PlayerSetup{ diff --git a/server/logic/engine.go b/server/logic/engine.go index 7eaa000..bd1b6bf 100644 --- a/server/logic/engine.go +++ b/server/logic/engine.go @@ -88,7 +88,7 @@ func (e *GameEngine) CheckGameOver(state *core.GameState) bool { if len(alive) == 1 { winnerID = alive[0] } else if len(alive) == 0 { - winnerID = "draw" + winnerID = state.LastDeadPlayerID } state.WinnerID = winnerID state.GameStarted = false @@ -102,7 +102,8 @@ func (e *GameEngine) CheckGameOver(state *core.GameState) bool { } if winnerPlayer != nil && winnerPlayer.RealUserID > 0 { - config.SettleGameWithBackend(e.Logger, winnerPlayer.RealUserID, winnerPlayer.Ticket, "", true, 100) + // 分数参数暂时传宝箱数,后端奖励由配置决定 + config.SettleGameWithBackend(e.Logger, winnerPlayer.RealUserID, winnerPlayer.Ticket, "", true, winnerPlayer.ChestCount) } else { e.Logger.Error("Winner player %s has no RealUserID, cannot settle", winnerID) } diff --git a/server/logic/logic_test.go b/server/logic/logic_test.go index 98011ee..bc6eab4 100644 --- a/server/logic/logic_test.go +++ b/server/logic/logic_test.go @@ -38,7 +38,7 @@ func createTestEngine() (*GameEngine, *core.GameState) { charMgr := characters.NewCharacterManager(nil) itemMgr := items.NewItemManager() - engine := NewGameEngine(logger, dispatcher, charMgr, itemMgr) + engine := NewGameEngine(logger, dispatcher, charMgr, itemMgr, nil, nil) // Create simplified state p1 := &core.Player{UserID: "p1", Username: "P1", HP: 4, MaxHP: 4, Character: "dog", RevealedCells: make(map[int]string)} diff --git a/server/logic/scenario_test.go b/server/logic/scenario_test.go index b6cc517..ebda134 100644 --- a/server/logic/scenario_test.go +++ b/server/logic/scenario_test.go @@ -81,7 +81,7 @@ func createScenarioState(scenario GameScenario) (*GameEngine, *core.GameState) { dispatcher := &MockDispatcher{} charMgr := characters.NewCharacterManager(nil) itemMgr := items.NewItemManager() - engine := NewGameEngine(logger, dispatcher, charMgr, itemMgr) + engine := NewGameEngine(logger, dispatcher, charMgr, itemMgr, nil, nil) // 创建玩家 players := make(map[string]*core.Player) @@ -406,7 +406,7 @@ func TestScenario_SafeAreaExpansion(t *testing.T) { dispatcher := &MockDispatcher{} charMgr := characters.NewCharacterManager(nil) itemMgr := items.NewItemManager() - engine := NewGameEngine(logger, dispatcher, charMgr, itemMgr) + engine := NewGameEngine(logger, dispatcher, charMgr, itemMgr, nil, nil) // 创建简单网格测试扩散 // 布局 (3x3): diff --git a/tempo/tempo-config.yaml b/tempo/tempo-config.yaml new file mode 100644 index 0000000..e3de3ef --- /dev/null +++ b/tempo/tempo-config.yaml @@ -0,0 +1,38 @@ +server: + http_listen_port: 3200 + +distributor: + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +ingester: + trace_idle_period: 10s + max_block_bytes: 1_000_000 + max_block_duration: 5m + +compactor: + compaction: + compaction_window: 1h + max_block_bytes: 100_000_000 + block_retention: 48h + compacted_block_retention: 10m + +storage: + trace: + backend: local + local: + path: /var/tempo/traces + wal: + path: /var/tempo/wal + +metrics_generator: + registry: + external_labels: + source: tempo + storage: + path: /var/tempo/generator/wal