175 lines
5.4 KiB
Ruby
175 lines
5.4 KiB
Ruby
class Game < ApplicationRecord
|
||
has_many :turns, dependent: :destroy
|
||
has_many :participants, dependent: :destroy
|
||
has_many :users, through: :participants
|
||
has_many :boards, dependent: :destroy
|
||
has_one :latest_turn, -> { order(number: :desc) }, class_name: "Turn"
|
||
|
||
# パスワード保護
|
||
has_secure_password :password, validations: false
|
||
|
||
# バリデーション
|
||
validates :status, inclusion: {
|
||
in: %w[recruiting power_selection in_progress finished cancelled]
|
||
}
|
||
validates :auto_order_mode, inclusion: { in: %w[hold random] }
|
||
validates :participants_count,
|
||
numericality: { greater_than_or_equal_to: 2, less_than_or_equal_to: 7 },
|
||
unless: :is_solo_mode?
|
||
|
||
# ハウスルールバリデーション
|
||
validates :year_limit,
|
||
numericality: { only_integer: true, greater_than_or_equal_to: 1901, less_than_or_equal_to: 1999 },
|
||
allow_nil: true
|
||
validates :victory_sc_count,
|
||
numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 34 }
|
||
validates :scoring_system, inclusion: { in: %w[none sc_count sc_ratio dss sos] }
|
||
|
||
# ターンスケジュールバリデーション
|
||
validate :validate_turn_schedule
|
||
|
||
# コールバック
|
||
after_create :create_global_board
|
||
|
||
# ヘルパーメソッド
|
||
def password_protected?
|
||
password_digest.present?
|
||
end
|
||
|
||
def solo_mode?
|
||
is_solo_mode
|
||
end
|
||
|
||
def administrator
|
||
participants.find_by(is_administrator: true)&.user
|
||
end
|
||
|
||
def available_powers
|
||
assigned_powers = participants.where.not(power: nil).pluck(:power)
|
||
%w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY] - assigned_powers
|
||
end
|
||
|
||
def all_powers_assigned?
|
||
participants.where(power: nil).empty? &&
|
||
participants.count == participants_count
|
||
end
|
||
|
||
def all_orders_submitted?
|
||
participants.where(power: nil).empty? &&
|
||
participants.all?(&:orders_submitted)
|
||
end
|
||
|
||
def unassigned_powers
|
||
all_powers = %w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY]
|
||
assigned_powers = participants.where.not(power: nil).pluck(:power)
|
||
all_powers - assigned_powers
|
||
end
|
||
|
||
# ターンスケジュール関連メソッド
|
||
|
||
def auto_turn?
|
||
turn_schedule.present?
|
||
end
|
||
|
||
# 次のデッドラインを計算(日本時間基準)
|
||
def calculate_next_deadline
|
||
return nil unless turn_schedule.present?
|
||
|
||
hours = turn_schedule.split(",").map(&:strip).map(&:to_i).sort
|
||
now = Time.current.in_time_zone("Asia/Tokyo")
|
||
|
||
# 今日の残りの時間枠を探す(JST基準)
|
||
next_time = hours.map { |h| now.beginning_of_day + h.hours }
|
||
.find { |t| t > now }
|
||
|
||
# 今日の枠がなければ翌日の最初の枠
|
||
next_time || (now.beginning_of_day + 1.day + hours.first.hours)
|
||
end
|
||
|
||
# スケジュール表示用
|
||
def schedule_display
|
||
return "手動" unless turn_schedule.present?
|
||
hours = turn_schedule.split(",").map(&:strip)
|
||
"毎日 " + hours.map { |h| "#{h}時" }.join("・")
|
||
end
|
||
|
||
# ハウスルール関連メソッド
|
||
|
||
# 年数制限チェック: フェーズ名から年を抽出し、year_limitを超えているか判定
|
||
def year_limit_reached?(phase_name)
|
||
return false unless year_limit.present? && phase_name.present?
|
||
|
||
# フェーズ名の例: "S1901M", "F1910R", "W1901A"
|
||
year_match = phase_name.match(/[SFW](\d{4})[MRA]/)
|
||
return false unless year_match
|
||
|
||
year_match[1].to_i > year_limit
|
||
end
|
||
|
||
# ソロ勝利判定: いずれかの国のSC数がvictory_sc_count以上か
|
||
def solo_victory?(game_state)
|
||
centers = game_state&.dig("centers") || {}
|
||
centers.any? { |_power, scs| scs.size >= victory_sc_count }
|
||
end
|
||
|
||
# ソロ勝利した国を返す
|
||
def solo_victory_power(game_state)
|
||
centers = game_state&.dig("centers") || {}
|
||
centers.find { |_power, scs| scs.size >= victory_sc_count }&.first
|
||
end
|
||
|
||
# スコア計算
|
||
def calculate_scores(game_state)
|
||
return {} if scoring_system == "none"
|
||
|
||
centers = game_state&.dig("centers") || {}
|
||
total_scs = centers.values.flatten.size
|
||
alive_count = centers.count { |_power, scs| scs.any? }
|
||
|
||
case scoring_system
|
||
when "sc_count"
|
||
# 単純SC数
|
||
centers.transform_values { |scs| scs.size }
|
||
when "sc_ratio"
|
||
# SC比率(%)
|
||
centers.transform_values { |scs| total_scs > 0 ? (scs.size.to_f / total_scs * 100).round(1) : 0 }
|
||
when "dss"
|
||
# Draw Size Scoring: 生存国で均等分割
|
||
centers.transform_values { |scs| scs.any? ? (100.0 / alive_count).round(1) : 0 }
|
||
when "sos"
|
||
# Sum of Squares
|
||
sum_of_squares = centers.values.sum { |scs| scs.size ** 2 }.to_f
|
||
centers.transform_values { |scs| sum_of_squares > 0 ? ((scs.size ** 2) / sum_of_squares * 100).round(1) : 0 }
|
||
else
|
||
{}
|
||
end
|
||
end
|
||
|
||
# スコアリング方式の日本語名
|
||
def scoring_system_name
|
||
case scoring_system
|
||
when "none" then "なし"
|
||
when "sc_count" then "SC数"
|
||
when "sc_ratio" then "SC比率"
|
||
when "dss" then "DSS(均等分割)"
|
||
when "sos" then "SoS(二乗和)"
|
||
else scoring_system
|
||
end
|
||
end
|
||
|
||
private
|
||
|
||
def validate_turn_schedule
|
||
return if turn_schedule.blank?
|
||
|
||
hours = turn_schedule.split(",").map(&:strip)
|
||
unless hours.all? { |h| h.match?(/\A\d{1,2}\z/) && h.to_i.between?(0, 23) }
|
||
errors.add(:turn_schedule, "は0〜23の数値をカンマ区切りで入力してください(例: 0,18)")
|
||
end
|
||
end
|
||
|
||
def create_global_board
|
||
boards.create!(board_type: "global", is_public: true)
|
||
end
|
||
end
|