#!/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.138: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