425 lines
14 KiB
Ruby
425 lines
14 KiB
Ruby
class GamesController < ApplicationController
|
||
include GamesHelper
|
||
before_action :set_game, only: %i[ show edit update destroy join_game start_power_selection start_order_input turn_data vote_draw force_draw ]
|
||
before_action :require_login, only: %i[ new create join_game ]
|
||
before_action :require_game_admin, only: %i[ edit update destroy start_power_selection start_order_input ]
|
||
|
||
helper_method :get_available_powers_for_select
|
||
|
||
# GET /games or /games.json
|
||
def index
|
||
@recruiting_games = Game.where(status: "recruiting").includes(:participants)
|
||
@my_games = current_user ? Game.joins(:participants).where(participants: { user_id: current_user.id }).includes(:participants, :turns) : []
|
||
@games = Game.all.includes(:participants, :turns)
|
||
end
|
||
|
||
# GET /games/1 or /games/1.json
|
||
def show
|
||
@latest_turn = @game.turns.last
|
||
|
||
# ゲーム終了判定
|
||
@game_finished = @game.status == "finished"
|
||
|
||
# 表示するターンの決定
|
||
if @game_finished
|
||
# 終了済みの場合:
|
||
# params[:turn_number] があればそのターン
|
||
# なければ 最初のターン (Turn 1) を表示
|
||
if params[:turn_number].present?
|
||
@display_turn = @game.turns.find_by(number: params[:turn_number].to_i)
|
||
end
|
||
|
||
# 指定がない、または見つからない場合は初期ターン(number=1)を表示
|
||
# もし存在しなければ最新(というかあるやつ)
|
||
@display_turn ||= @game.turns.find_by(number: 1) || @latest_turn
|
||
|
||
# 最終結果の取得 (最後のターン情報から)
|
||
if @latest_turn.game_state
|
||
centers = @latest_turn.game_state["centers"] || {}
|
||
alive_powers = centers.keys
|
||
|
||
# ソロ勝利判定
|
||
solo_winner = @game.solo_victory_power(@latest_turn.game_state)
|
||
if solo_winner
|
||
result_type = "Solo Victory"
|
||
winners = [ solo_winner ]
|
||
else
|
||
result_type = "Draw"
|
||
winners = alive_powers
|
||
end
|
||
|
||
@winner_info = {
|
||
type: result_type,
|
||
winners: winners,
|
||
scores: @game.calculate_scores(@latest_turn.game_state)
|
||
}
|
||
end
|
||
else
|
||
# 進行中の場合: params[:turn_number] があればそのターン、なければ最新ターンを表示
|
||
if params[:turn_number].present?
|
||
@display_turn = @game.turns.find_by(number: params[:turn_number].to_i)
|
||
end
|
||
@display_turn ||= @latest_turn
|
||
end
|
||
|
||
if @display_turn
|
||
@game_state = @display_turn.game_state
|
||
|
||
# フェーズ名のパース
|
||
@current_season_year = parse_phase(@display_turn.phase)
|
||
|
||
# 国別情報の集計 (表示対象ターンのデータ)
|
||
centers = @game_state["centers"] || {}
|
||
units = @game_state["units"] || {}
|
||
|
||
# 全7カ国(固定)
|
||
powers = %w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY]
|
||
|
||
# N+1 防止のためparticipantsをキャッシュ
|
||
participants_cache = @game.participants.includes(:user).index_by(&:power)
|
||
|
||
@country_statuses = powers.map do |power|
|
||
participant = participants_cache[power]
|
||
# 終了済みなら全員完了扱い、そうでなければターンごとの提出状況
|
||
submitted = @game_finished ? true : @display_turn.orders_submitted_for?(power)
|
||
|
||
{
|
||
power: power,
|
||
sc_count: centers[power]&.size || 0,
|
||
unit_count: units[power]&.size || 0,
|
||
submitted: submitted,
|
||
participant: participant,
|
||
is_user: current_user && participant&.user_id == current_user.id
|
||
}
|
||
end
|
||
|
||
# 自国を先頭に移動
|
||
if current_user
|
||
user_power_index = @country_statuses.find_index { |s| s[:is_user] }
|
||
if user_power_index
|
||
user_status = @country_statuses.delete_at(user_power_index)
|
||
@country_statuses.unshift(user_status)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
# GET /games/new
|
||
def new
|
||
@game = Game.new
|
||
@game.is_solo_mode = current_user&.admin? ? false : false
|
||
end
|
||
|
||
# GET /games/1/edit
|
||
def edit
|
||
end
|
||
|
||
# POST /games or /games.json
|
||
def create
|
||
@game = Game.new(game_params)
|
||
|
||
# ソロモードかどうかを判定(管理者のみ選択可能)
|
||
if current_user&.admin? && params.dig(:game, :game_mode) == "admin_mode"
|
||
@game.is_solo_mode = true
|
||
else
|
||
@game.is_solo_mode = false
|
||
end
|
||
|
||
respond_to do |format|
|
||
if @game.save
|
||
# ゲーム作成者は自動的に参加者として登録(管理者として)
|
||
Participant.create!(
|
||
game: @game,
|
||
user: current_user,
|
||
is_administrator: true
|
||
)
|
||
|
||
# ソロモードの場合、即座に最初のターンを作成
|
||
if @game.solo_mode?
|
||
service = GameSetupService.new(@game)
|
||
result = service.setup_initial_turn
|
||
if result[:success]
|
||
@game.update!(status: "in_progress")
|
||
else
|
||
# 失敗したらロールバックしたいところだが・・・
|
||
# 現状はsave後なので、エラー表示だけにするかdestroyするか。
|
||
# 今回は簡易的にログに残す
|
||
logger.error result[:message]
|
||
end
|
||
end
|
||
|
||
format.html { redirect_to @game, notice: "ゲームが正常に作成されました。" }
|
||
format.json { render :show, status: :created, location: @game }
|
||
else
|
||
format.html { render :new, status: :unprocessable_entity }
|
||
format.json { render json: @game.errors, status: :unprocessable_entity }
|
||
end
|
||
end
|
||
end
|
||
|
||
# PATCH/PUT /games/1 or /games/1.json
|
||
def update
|
||
respond_to do |format|
|
||
if @game.update(game_params)
|
||
# スケジュール変更時にデッドラインを再計算
|
||
if @game.status == "in_progress"
|
||
@game.update_column(:next_deadline_at, @game.auto_turn? ? @game.calculate_next_deadline : nil)
|
||
end
|
||
format.html { redirect_to @game, notice: "ゲームが正常に更新されました。", status: :see_other }
|
||
format.json { render :show, status: :ok, location: @game }
|
||
else
|
||
format.html { render :edit, status: :unprocessable_entity }
|
||
format.json { render json: @game.errors, status: :unprocessable_entity }
|
||
end
|
||
end
|
||
end
|
||
|
||
# DELETE /games/1 or /games/1.json
|
||
def destroy
|
||
@game.destroy!
|
||
|
||
respond_to do |format|
|
||
format.html { redirect_to games_path, notice: "ゲームが正常に削除されました。", status: :see_other }
|
||
format.json { head :no_content }
|
||
end
|
||
end
|
||
|
||
# POST /games/1/join
|
||
def join_game
|
||
unless current_user
|
||
redirect_to login_path, alert: "ゲームに参加するにはログインしてください"
|
||
return
|
||
end
|
||
|
||
# パスワードチェック
|
||
if @game.password_protected?
|
||
password = params.dig(:participant, :password)
|
||
unless @game.authenticate_password(password)
|
||
redirect_to @game, alert: "パスワードが正しくありません"
|
||
return
|
||
end
|
||
end
|
||
|
||
# 既に参加しているかチェック
|
||
if @game.participants.exists?(user: current_user)
|
||
redirect_to @game, alert: "既にこのゲームに参加しています"
|
||
return
|
||
end
|
||
|
||
# 満員チェック
|
||
if @game.participants.count >= @game.participants_count
|
||
redirect_to @game, alert: "このゲームは満員です"
|
||
return
|
||
end
|
||
|
||
# 参加者を作成
|
||
Participant.create!(
|
||
game: @game,
|
||
user: current_user
|
||
)
|
||
|
||
# 定員到達チェック
|
||
if @game.participants.count == @game.participants_count
|
||
@game.update(status: "power_selection")
|
||
end
|
||
|
||
redirect_to @game, notice: "ゲームに正常に参加しました!"
|
||
end
|
||
|
||
# POST /games/1/start_power_selection
|
||
def start_power_selection
|
||
unless @game.participants.count >= 2
|
||
redirect_to @game, alert: "最低2人の参加者が必要です"
|
||
return
|
||
end
|
||
|
||
@game.update!(status: "power_selection")
|
||
redirect_to @game, notice: "国選択フェーズを開始しました!"
|
||
end
|
||
|
||
# POST /games/1/start_order_input
|
||
def start_order_input
|
||
unless @game.all_powers_assigned?
|
||
redirect_to @game, alert: "全員が国を選択する必要があります"
|
||
return
|
||
end
|
||
|
||
# 最初のターンを作成
|
||
service = GameSetupService.new(@game)
|
||
result = service.setup_initial_turn
|
||
|
||
if result[:success]
|
||
update_attrs = { status: "in_progress" }
|
||
update_attrs[:next_deadline_at] = @game.calculate_next_deadline if @game.auto_turn?
|
||
@game.update!(update_attrs)
|
||
redirect_to @game, notice: "命令入力フェーズを開始しました!"
|
||
else
|
||
redirect_to @game, alert: result[:message]
|
||
end
|
||
end
|
||
|
||
# GET /games/1/turn_data.json
|
||
def turn_data
|
||
if params[:turn_number].present?
|
||
target_turn = @game.turns.find_by(number: params[:turn_number])
|
||
end
|
||
target_turn ||= @game.turns.last
|
||
|
||
return head :not_found unless target_turn
|
||
|
||
# 国別ステータス情報の再計算(JSON用)
|
||
# showアクションと同様のロジック
|
||
game_state = target_turn.game_state
|
||
centers = game_state["centers"] || {}
|
||
units = game_state["units"] || {}
|
||
powers = %w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY]
|
||
|
||
country_statuses = powers.map do |power|
|
||
# 終了済みなら全員完了扱い
|
||
submitted = @game.status == "finished" ? true : target_turn.orders_submitted_for?(power)
|
||
{
|
||
power: power,
|
||
sc_count: centers[power]&.size || 0,
|
||
unit_count: units[power]&.size || 0,
|
||
submitted: submitted
|
||
}
|
||
end
|
||
|
||
render json: {
|
||
turn_number: target_turn.number,
|
||
phase: target_turn.phase,
|
||
possible_orders: target_turn.possible_orders,
|
||
decided_orders: target_turn.orders || {},
|
||
svg_orders: target_turn.svg_orders || {},
|
||
# svg_date カラムにデフォルトSVGが入っていると仮定(または svg_orders["NONE"])
|
||
# ここでは svg_date が単一の画像パスかSVG文字列かによるが、
|
||
# 既存の show.html.erb 実装を見ると svg_orders["NONE"] を使っている可能性が高い
|
||
# 既存実装: default_svg: last_turn.svg_date となっていたのでそのまま変更なしでいくが
|
||
# target_turn.svg_date を返すようにする
|
||
default_svg: target_turn.svg_date,
|
||
|
||
# 完了状況
|
||
all_orders_submitted: @game.status == "finished" || @game.all_orders_submitted?,
|
||
missing_orders_powers: @game.status == "finished" ? [] : @game.participants.where(orders_submitted: false).pluck(:power).compact,
|
||
country_statuses: country_statuses
|
||
}
|
||
end
|
||
|
||
def vote_draw
|
||
return unless @game.status == "in_progress"
|
||
|
||
turn = @game.turns.last
|
||
participant = @game.participants.find_by(user: current_user)
|
||
|
||
unless participant && participant.power
|
||
redirect_to @game, alert: "権限がありません。"
|
||
return
|
||
end
|
||
|
||
power = participant.power
|
||
if turn.draw_voted?(power)
|
||
turn.revoke_draw_vote(power)
|
||
flash[:notice] = "引き分け投票を取り消しました。"
|
||
else
|
||
turn.vote_draw(power)
|
||
flash[:notice] = "引き分けに投票しました。"
|
||
end
|
||
|
||
if turn.unanimous_draw?
|
||
if execute_draw(turn)
|
||
flash[:notice] = "全会一致により、ゲームは引き分けとなりました。"
|
||
else
|
||
# エラーメッセージは execute_draw 内で flash[:alert] に設定される
|
||
end
|
||
end
|
||
|
||
redirect_to @game
|
||
end
|
||
|
||
def force_draw
|
||
return unless current_user.admin?
|
||
|
||
turn = @game.turns.last
|
||
if execute_draw(turn)
|
||
redirect_to @game, notice: "ゲームを強制的に引き分けにしました。"
|
||
else
|
||
redirect_to @game
|
||
end
|
||
end
|
||
|
||
private
|
||
|
||
def execute_draw(turn)
|
||
client = GameApiClient.new
|
||
# 生存国を勝者として扱う(引き分け)
|
||
winners = turn.powers
|
||
begin
|
||
response = client.api_game_draw(turn.game_state, winners: winners)
|
||
|
||
if response
|
||
# 新しいターン(完了状態)を作成
|
||
Turn.create!(
|
||
game: @game,
|
||
number: turn.number + 1,
|
||
# year, season カラムは存在しないため削除
|
||
# phase を COMPLETED に設定
|
||
game_state: response,
|
||
orders: {},
|
||
possible_orders: {},
|
||
phase: "COMPLETED",
|
||
svg_date: turn.svg_date,
|
||
svg_orders: turn.svg_orders
|
||
)
|
||
@game.update(status: "finished")
|
||
true
|
||
else
|
||
flash[:alert] = "ゲームサーバーからの応答が不正です。"
|
||
false
|
||
end
|
||
rescue Faraday::ConnectionFailed => e
|
||
flash[:alert] = "ゲームサーバーへの接続に失敗しました。管理者へ連絡してください。"
|
||
Rails.logger.error "API Connection Failed: #{e.message}"
|
||
false
|
||
rescue Faraday::TimeoutError => e
|
||
flash[:alert] = "ゲームサーバーとの通信がタイムアウトしました。"
|
||
Rails.logger.error "API Timeout: #{e.message}"
|
||
false
|
||
rescue StandardError => e
|
||
flash[:alert] = "予期せぬエラーが発生しました: #{e.message}"
|
||
Rails.logger.error "Execute Draw Error: #{e.message}"
|
||
false
|
||
end
|
||
end
|
||
|
||
|
||
|
||
def set_game
|
||
@game = Game.find(params.expect(:id))
|
||
end
|
||
|
||
def require_game_admin
|
||
unless current_user&.admin? || @game.administrator == current_user
|
||
redirect_to @game, alert: "ゲーム管理者のみこの機能にアクセスできます"
|
||
end
|
||
end
|
||
|
||
def game_params
|
||
house_rules = [ :year_limit, :victory_sc_count, :scoring_system, :turn_schedule ]
|
||
|
||
if action_name == "update"
|
||
params.expect(game: [ :title, :memo, :auto_order_mode ] + house_rules)
|
||
else
|
||
permitted = [ :title, :memo ] + house_rules
|
||
|
||
if current_user&.admin?
|
||
permitted += [ :participants_count, :password, :auto_order_mode ]
|
||
else
|
||
# 一般ユーザーはマルチプレイヤーモードのみ
|
||
permitted += [ :participants_count, :password, :auto_order_mode ]
|
||
end
|
||
|
||
params.expect(game: permitted)
|
||
end
|
||
end
|
||
end
|