フロントエンドプレイアブル
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
2026-02-15 14:57:17 +09:00
commit f25fd6f802
198 changed files with 10342 additions and 0 deletions

View File

@@ -0,0 +1,421 @@
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