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] @country_statuses = powers.map do |power| participant = @game.participants.find_by(power: 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