#!/usr/bin/env ruby # -*- coding: utf-8 -*- require 'em-websocket' require 'set' require 'json' require 'uri' require 'time' # WebSocket ポート WS_PORT = 8804 # --- 変数定義 --- clients = Set.new participants = {} team_points = Hash.new(0) raised_queue = [] selected_url = nil penalty_box = {} # --- ヘルパーメソッド --- 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 def broadcast_penalty_info(clients, penalty_hash) payload = { type: "penalty_update", penalties: penalty_hash }.to_json clients.each { |c| c.send(payload) } end # --- サーバー起動 --- trap('INT') do puts "\n[INTERRUPT] シャットダウン中..." EM.stop if EM.reactor_running? end puts "WebSocket Server starting on port #{WS_PORT}..." EM.run do # 30秒ごとにPingを送信 EM.add_periodic_timer(30) do clients.each { |ws| ws.ping if ws.state == :connected } end # 1秒ごとにペナルティ情報を更新・通知 EM.add_periodic_timer(1) do penalty_box.delete_if { |group, expiry_time| Time.now >= expiry_time } broadcast_penalty_info(clients, penalty_box) end EM::WebSocket.start(host: "0.0.0.0", port: WS_PORT) do |ws_conn| ws_conn.onopen do puts "[OPEN] クライアント接続: #{clients.size + 1} 名" 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) 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 group = participant_info[:group] # ペナルティ中か? (penalty_boxに存在し、かつ、現在時刻 < 解除時刻) if penalty_box[group] && Time.now < penalty_box[group] puts "[PENALTY] #{group} はペナルティ中のため、#{participant_info[:name]} さんの挙手をブロックしました。" elsif raised_queue.any? { |p| p[:group] == group } puts "[REJECT] #{group} は既にキューにいるため、#{participant_info[:name]} さんの挙手をブロックしました。" else raised_queue << participant_info puts "[RAISE] #{group} の #{participant_info[:name]} さんが挙手。" broadcast_queue(clients, raised_queue) end end # (その他のcaseは変更なし) # ... when "reset" raised_queue.clear penalty_box.clear puts "[RESET] 挙手者キューとペナルティをリセットしました" broadcast_queue(clients, raised_queue) clients.each { |c| c.send({ type: "stop" }.to_json) } when "correct" if first_raiser = raised_queue.first group = first_raiser[:group] team_points[group] += 5 puts "[CORRECT] #{group} に 5ポイント加算! 合計: #{team_points[group]}点" payload = { type: "correct", answer: data["answer"], raiser: first_raiser }.to_json clients.each { |c| c.send(payload) } raised_queue.clear penalty_box.clear broadcast_teams_and_points(clients, participants, team_points) broadcast_queue(clients, raised_queue) end when "incorrect" if incorrect_participant = raised_queue.shift group = incorrect_participant[:group] penalty_until = Time.now + 8 penalty_box[group] = penalty_until puts "[INCORRECT] #{group} は #{penalty_until.strftime('%H:%M:%S')} までペナルティ。" broadcast_penalty_info(clients, penalty_box) clients.each { |c| c.send({type: "incorrect"}.to_json) } EM.add_timer(1) do puts "[TIMER] 1秒経過..." if raised_queue.empty? puts "[AUTO PLAY] 回答者がいないため、再生を再開します。" clients.each { |c| c.send({ type: "play" }.to_json) } broadcast_queue(clients, raised_queue) else puts "更新されたキューを送信します。" broadcast_queue(clients, raised_queue) end end end when "select" selected_url = "http://192.168.1.42:8890/#{URI.encode_uri_component(data["file"])}" penalty_box.clear clients.each { |c| c.send({ type: "select", url: selected_url }.to_json) } when "reset_scores" team_points.clear puts "[SCORE RESET] 全チームの得点をリセットしました。" broadcast_teams_and_points(clients, participants, team_points) when "adjust_score" group = data["group"] score = data["score"].to_i if group && score team_points[group] += score puts "[ADJUST SCORE] #{group} の得点を #{score}点 調整。合計: #{team_points[group]}点" broadcast_teams_and_points(clients, participants, team_points) end 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) } when "show_answer" clients.each { |c| c.send(msg) } else puts "[WARN] 未知のメッセージタイプを受信: #{data['type']}" end rescue JSON::ParserError => e puts "[ERROR] JSONパースエラー: #{e.message}" end end ws_conn.onclose do puts "[CLOSE] 切断: 残り #{clients.size - 1} 名" clients.delete(ws_conn) if participant = participants.delete(ws_conn) puts "[LEAVE] #{participant[:group]} の #{participant[:name]} さんが退出しました。" if raised_queue.delete(participant) puts "切断した参加者がキューにいたため、キュー情報を更新します。" broadcast_queue(clients, raised_queue) end broadcast_teams_and_points(clients, participants, team_points) end end end end