フロントエンドプレイアブル
This commit is contained in:
3
app/models/application_record.rb
Normal file
3
app/models/application_record.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
primary_abstract_class
|
||||
end
|
||||
0
app/models/concerns/.keep
Normal file
0
app/models/concerns/.keep
Normal file
165
app/models/game.rb
Normal file
165
app/models/game.rb
Normal file
@@ -0,0 +1,165 @@
|
||||
class Game < ApplicationRecord
|
||||
has_many :turns, dependent: :destroy
|
||||
has_many :participants, dependent: :destroy
|
||||
has_many :users, through: :participants
|
||||
|
||||
# パスワード保護
|
||||
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
|
||||
|
||||
# ヘルパーメソッド
|
||||
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
|
||||
end
|
||||
52
app/models/game_participant.rb
Normal file
52
app/models/game_participant.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
class GameParticipant < ApplicationRecord
|
||||
belongs_to :game
|
||||
belongs_to :user
|
||||
|
||||
validates :status, inclusion: { in: %w[joined ready finished] }
|
||||
validates :user_id, uniqueness: { scope: :game_id }
|
||||
validates :power, uniqueness: { scope: :game_id }, allow_nil: true
|
||||
|
||||
# ステータス管理メソッド
|
||||
def joined?
|
||||
status == 'joined'
|
||||
end
|
||||
|
||||
def ready?
|
||||
status == 'ready'
|
||||
end
|
||||
|
||||
def finished?
|
||||
status == 'finished'
|
||||
end
|
||||
|
||||
# パワー選択関連
|
||||
def has_selected_power?
|
||||
power.present?
|
||||
end
|
||||
|
||||
def select_power!(power_name)
|
||||
update!(power: power_name, status: 'ready')
|
||||
end
|
||||
|
||||
# ゲーム参加メソッド
|
||||
class << self
|
||||
def join_game!(game, user, password = nil)
|
||||
# パスワードチェック(プレイヤーモードの場合)
|
||||
if game.player_mode? && game.password.present?
|
||||
raise "Invalid password" unless game.password == password
|
||||
end
|
||||
|
||||
# 既存参加チェック
|
||||
if game.game_participants.exists?(user_id: user.id)
|
||||
raise "Already joined this game"
|
||||
end
|
||||
|
||||
# 定員チェック
|
||||
if game.player_mode? && game.full?
|
||||
raise "Game is full"
|
||||
end
|
||||
|
||||
create!(game: game, user: user, joined_at: Time.current, status: 'joined')
|
||||
end
|
||||
end
|
||||
end
|
||||
20
app/models/participant.rb
Normal file
20
app/models/participant.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class Participant < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :game
|
||||
|
||||
# バリデーション
|
||||
validates :user_id, uniqueness: {
|
||||
scope: :game_id,
|
||||
message: "既にこのゲームに参加しています"
|
||||
}
|
||||
|
||||
validates :power, uniqueness: {
|
||||
scope: :game_id,
|
||||
message: "この国は既に選択されています"
|
||||
}, allow_nil: true
|
||||
|
||||
validates :power, inclusion: {
|
||||
in: %w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY],
|
||||
message: "無効な国です"
|
||||
}, allow_nil: true
|
||||
end
|
||||
124
app/models/turn.rb
Normal file
124
app/models/turn.rb
Normal file
@@ -0,0 +1,124 @@
|
||||
class Turn < ApplicationRecord
|
||||
belongs_to :game
|
||||
|
||||
# 特定の国の実行可能な命令を取得
|
||||
# 例: turn.possible_orders_for("FRANCE")
|
||||
def possible_orders_for(power_name)
|
||||
possible_orders&.dig("possible_orders", power_name.to_s.upcase)
|
||||
end
|
||||
|
||||
# 特定の国の決定済み命令を取得
|
||||
# 例: turn.orders_for("FRANCE")
|
||||
def orders_for(power_name)
|
||||
# orders は直接ハッシュを保存する場合を想定(必要に応じてネストを調整)
|
||||
orders&.dig(power_name.to_s.upcase)
|
||||
end
|
||||
|
||||
# 命令が存在するすべての国名リストを取得
|
||||
def powers
|
||||
possible_orders&.dig("possible_orders")&.keys || []
|
||||
end
|
||||
|
||||
# 特定の国の命令が提出済みかチェック
|
||||
def orders_submitted_for?(power_name)
|
||||
orders&.key?(power_name.to_s.upcase)
|
||||
end
|
||||
|
||||
# 未提出の国のリストを取得
|
||||
def pending_powers
|
||||
game.participants.where.not(power: nil).map(&:power).reject do |power|
|
||||
orders_submitted_for?(power)
|
||||
end
|
||||
end
|
||||
|
||||
# 自動命令を生成(参加者がいない国用)
|
||||
def generate_auto_orders_for_unassigned_powers
|
||||
unassigned = game.unassigned_powers
|
||||
return if unassigned.empty?
|
||||
|
||||
client = GameApiClient.new
|
||||
current_orders = orders || {}
|
||||
|
||||
unassigned.each do |power|
|
||||
if game.auto_order_mode == "hold"
|
||||
# HOLD命令を生成
|
||||
current_orders[power] = generate_hold_orders(power)
|
||||
else
|
||||
# ランダム命令を生成
|
||||
auto_orders_response = client.api_calculate_auto_orders(game_state, power)
|
||||
if auto_orders_response
|
||||
# APIレスポンスから命令を抽出
|
||||
# レスポンスが {"orders": [...]} の形式の場合
|
||||
if auto_orders_response.is_a?(Hash) && auto_orders_response["orders"]
|
||||
current_orders[power] = auto_orders_response["orders"]
|
||||
elsif auto_orders_response.is_a?(Array)
|
||||
current_orders[power] = auto_orders_response
|
||||
else
|
||||
# フォールバック: HOLD命令を使用
|
||||
current_orders[power] = generate_hold_orders(power)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
update(orders: current_orders)
|
||||
update(orders: current_orders)
|
||||
end
|
||||
|
||||
# 引き分け投票
|
||||
def vote_draw(power_name)
|
||||
current_votes = draw_votes || []
|
||||
power = power_name.to_s.upcase
|
||||
unless current_votes.include?(power)
|
||||
current_votes << power
|
||||
update(draw_votes: current_votes)
|
||||
end
|
||||
end
|
||||
|
||||
# 引き分け投票取り消し
|
||||
def revoke_draw_vote(power_name)
|
||||
current_votes = draw_votes || []
|
||||
power = power_name.to_s.upcase
|
||||
if current_votes.include?(power)
|
||||
current_votes.delete(power)
|
||||
update(draw_votes: current_votes)
|
||||
end
|
||||
end
|
||||
|
||||
# 投票済みかチェック
|
||||
def draw_voted?(power_name)
|
||||
(draw_votes || []).include?(power_name.to_s.upcase)
|
||||
end
|
||||
|
||||
# 全会一致で引き分けかチェック
|
||||
def unanimous_draw?
|
||||
# 生存している国
|
||||
active_powers = powers
|
||||
return false if active_powers.empty?
|
||||
|
||||
# プレイヤーが割り当てられている国のみを対象とする
|
||||
# (未割り当ての国は投票できないため、除外する)
|
||||
assigned_powers = game.participants.where.not(power: nil).pluck(:power).map(&:upcase)
|
||||
|
||||
# 投票権を持つ国 = 生存している かつ プレイヤーがいる
|
||||
eligible_powers = active_powers.map(&:upcase) & assigned_powers
|
||||
|
||||
return false if eligible_powers.empty?
|
||||
|
||||
current_votes = draw_votes || []
|
||||
eligible_powers.all? { |power| current_votes.include?(power) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_hold_orders(power)
|
||||
possible = possible_orders_for(power)
|
||||
return {} unless possible
|
||||
|
||||
hold_orders = {}
|
||||
possible.each do |unit, moves|
|
||||
hold_orders[unit] = [ "H" ] if moves.is_a?(Array)
|
||||
end
|
||||
hold_orders
|
||||
end
|
||||
end
|
||||
15
app/models/user.rb
Normal file
15
app/models/user.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
class User < ApplicationRecord
|
||||
has_secure_password
|
||||
|
||||
validates :username, presence: true, length: { minimum: 3, maximum: 50 }
|
||||
validates :email, presence: true,
|
||||
uniqueness: { case_sensitive: false },
|
||||
format: { with: URI::MailTo::EMAIL_REGEXP }
|
||||
validates :password, length: { minimum: 6 }, if: -> { new_record? || !password.nil? }
|
||||
|
||||
before_save { self.email = email.downcase }
|
||||
|
||||
def admin?
|
||||
admin
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user