#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'em-websocket'
require 'set'
require 'webrick'
require 'json'
require 'uri'
# WebSocket ポート
WS_PORT = 8888
# HTTP ポート
HTTP_PORT = 8889
# クライアント集合
clients = Set.new
# 参加者リスト { ws_conn => { group: "1班", name: "名前" } }
participants = {}
# ▼▼▼【追加】チームポイントを管理するハッシュ。デフォルト値を0に設定 ▼▼▼
team_points = Hash.new(0)
# 挙手者キュー
raised_queue = []
# 現在選択中の曲URL
selected_url = nil
# === HTTPサーバー ===
http_server = WEBrick::HTTPServer.new(
Port: HTTP_PORT, BindAddress: '0.0.0.0', DocumentRoot: './public',
AccessLog: [], Logger: WEBrick::Log.new("/dev/null")
)
Thread.new { http_server.start }
puts "WebSocket server starting on port #{WS_PORT}..."
puts "HTTP server (for HTML/CSS) running on http://localhost:#{HTTP_PORT}/"
# ▼▼▼【変更】チーム情報とポイントをブロードキャストするヘルパーメソッド ▼▼▼
def broadcast_teams_and_points(clients, participants_hash, points_hash)
team_list = participants_hash.values.sort_by { |p| [p[:group], p[:name]] }
payload = { type: 'update_teams', teams: team_list, points: points_hash }.to_json
clients.each { |c| c.send(payload) }
puts "[BROADCAST] チームとポイント情報を更新しました。"
end
# 挙手キューをブロードキャストするヘルパーメソッド
def broadcast_queue(clients, queue)
payload = { type: "announce", queue: queue }.to_json
clients.each { |c| c.send(payload) }
end
trap('INT') do
puts "\n[INTERRUPT] シャットダウン中..."
http_server.shutdown
EM.stop if EM.reactor_running?
end
# WebSocket サーバー
EM.run do
EM::WebSocket.start(host: "0.0.0.0", port: WS_PORT) do |ws_conn|
ws_conn.onopen do
clients << ws_conn
ws_conn.send({ type: "info", message: "うっす! WebSocket 接続完了" }.to_json)
# ▼▼▼【変更】接続時に現在のチームとポイント、挙手状況を送信 ▼▼▼
ws_conn.send({ type: 'update_teams', teams: participants.values.sort_by{|p| [p[:group], p[:name]]}, points: team_points }.to_json)
ws_conn.send({ type: "announce", queue: raised_queue }.to_json)
puts "[OPEN] クライアント接続: #{clients.size} 名"
end
ws_conn.onmessage do |msg|
puts "[RECV] #{msg}"
begin
data = JSON.parse(msg)
case data["type"]
when "join"
participants[ws_conn] = { group: data["group"], name: data["name"] }
puts "[JOIN] #{data['group']} の #{data['name']} さんが参加しました。"
broadcast_teams_and_points(clients, participants, team_points)
when "raise"
participant_info = participants[ws_conn]
if participant_info && !raised_queue.include?(participant_info)
raised_queue << participant_info
puts "[RAISE] #{participant_info[:group]} の #{participant_info[:name]} さんが挙手。"
broadcast_queue(clients, raised_queue)
end
when "reset"
raised_queue.clear
puts "[RESET] 挙手者キューをリセットしました"
broadcast_queue(clients, raised_queue)
# ▼▼▼【変更】正解処理 ▼▼▼
when "correct"
if first_raiser = raised_queue.first
group = first_raiser[:group]
team_points[group] += 5
puts "[CORRECT] #{group} に 5ポイント加算! 合計: #{team_points[group]}点"
raised_queue.clear # 正解したのでキューはリセット
clients.each { |c| c.send(msg) } # 全員に正解音を鳴らすよう通知
broadcast_teams_and_points(clients, participants, team_points) # ポイント情報を更新
broadcast_queue(clients, raised_queue) # 空になったキューを通知
end
# ▼▼▼【変更】不正解処理 ▼▼▼
when "incorrect"
if raised_queue.shift # キューの先頭を削除
puts "[INCORRECT] 回答者がキューから削除されました。残り: #{raised_queue.size}名"
clients.each { |c| c.send(msg) } # 全員に不正解音を鳴らすよう通知
broadcast_queue(clients, raised_queue) # 更新されたキューを通知
end
# --- 以下は変更なし ---
when "select"
selected_url = "http://192.168.1.11:8890/#{URI.encode_www_form_component(data["file"])}"
clients.each { |c| c.send({ type: "select", url: selected_url }.to_json) }
when "play"
clients.each { |c| c.send({ type: "play" }.to_json) } if selected_url
when "pause"
clients.each { |c| c.send({ type: "pause" }.to_json) }
when "stop"
clients.each { |c| c.send({ type: "stop" }.to_json) }
else
puts "[WARN] 未知のメッセージタイプを受信: #{data['type']}"
end
rescue JSON::ParserError => e
puts "[ERROR] JSONパースエラー: #{e.message}"
end
end
ws_conn.onclose do
clients.delete(ws_conn)
if participant = participants.delete(ws_conn)
puts "[LEAVE] #{participant[:group]} の #{participant[:name]} さんが退出しました。"
raised_queue.delete(participant)
broadcast_teams_and_points(clients, participants, team_points)
broadcast_queue(clients, raised_queue)
end
puts "[CLOSE] 切断: 残り #{clients.size} 名"
end
end
end