フロントエンドプレイアブル
This commit is contained in:
126
app/services/game_api_client.rb
Normal file
126
app/services/game_api_client.rb
Normal file
@@ -0,0 +1,126 @@
|
||||
require "faraday"
|
||||
require "json"
|
||||
|
||||
class GameApiClient
|
||||
BASE_URL = "http://0.0.0.0:8000"
|
||||
|
||||
def initialize
|
||||
@connection = Faraday.new(url: BASE_URL) do |f|
|
||||
f.request :json # リクエストをJSON形式にする
|
||||
f.response :json # レスポンスをJSONとしてパースする
|
||||
f.adapter Faraday.default_adapter
|
||||
end
|
||||
end
|
||||
|
||||
# GET リクエスト: 初期状態の取得
|
||||
def api_game_initial_state(map_name = "standard")
|
||||
response = @connection.get("/game/initial-state", { map_name: map_name })
|
||||
handle_response(response, "game/initial-state")
|
||||
end
|
||||
|
||||
# POST リクエスト: ゲームのみを強制終了 (Draw) にする
|
||||
def api_game_draw(game_state, winners: nil)
|
||||
response = @connection.post("/game/draw") do |req|
|
||||
body = { game_state: game_state }
|
||||
body[:winners] = winners if winners
|
||||
req.body = body
|
||||
end
|
||||
handle_response(response, "game/draw")
|
||||
end
|
||||
|
||||
# GET リクエスト: 利用可能なマップの取得
|
||||
def api_maps
|
||||
response = @connection.get("/maps")
|
||||
handle_response(response, "maps")
|
||||
end
|
||||
|
||||
# GET /maps/{map_name}
|
||||
def api_maps_data(map_name)
|
||||
response = @connection.get("/maps/#{map_name}")
|
||||
handle_response(response, "maps/#{map_name}")
|
||||
end
|
||||
|
||||
# POST リクエスト: 可能な命令の計算
|
||||
def api_calculate_possible_orders(game_state, power_name: "", by_power: false)
|
||||
response = @connection.post("/calculate/possible-orders") do |req|
|
||||
req.params["power_name"] = power_name
|
||||
req.params["by_power"] = by_power
|
||||
req.body = { game_state: game_state }
|
||||
end
|
||||
handle_response(response, "calculate/possible-orders")
|
||||
end
|
||||
|
||||
# POST リクエスト: 命令を処理して次のフェーズへ進める
|
||||
def api_calculate_process(game_state, orders)
|
||||
normalized_orders = normalize_orders(orders)
|
||||
response = @connection.post("/calculate/process") do |req|
|
||||
req.body = { game_state: game_state, orders: normalized_orders }
|
||||
end
|
||||
handle_response(response, "calculate/process")
|
||||
end
|
||||
|
||||
# POST リクエスト: 命令の妥当性を検証する
|
||||
def api_calculate_validate(game_state, orders)
|
||||
normalized_orders = normalize_orders(orders)
|
||||
response = @connection.post("/calculate/validate") do |req|
|
||||
req.body = { game_state: game_state, orders: normalized_orders }
|
||||
end
|
||||
handle_response(response, "calculate/validate")
|
||||
end
|
||||
|
||||
# POST リクエスト: 特定勢力の命令を自動生成する
|
||||
def api_calculate_auto_orders(game_state, power_name)
|
||||
response = @connection.post("/calculate/auto-orders") do |req|
|
||||
req.params["power_name"] = power_name
|
||||
req.body = { game_state: game_state }
|
||||
end
|
||||
handle_response(response, "calculate/auto-orders")
|
||||
end
|
||||
|
||||
# POST リクエスト: マップをSVGとしてレンダリングする
|
||||
def api_render(game_state, orders: nil, incl_orders: true, incl_abbrev: true)
|
||||
# ordersのキーを文字列に変換し、値を配列に変換して正規化
|
||||
normalized_orders = normalize_orders(orders)
|
||||
|
||||
body = {
|
||||
game_state: game_state,
|
||||
orders: normalized_orders,
|
||||
incl_orders: incl_orders,
|
||||
incl_abbrev: incl_abbrev
|
||||
}
|
||||
Rails.logger.debug "API Render Request Body: #{body.inspect}"
|
||||
|
||||
response = @connection.post("/render") do |req|
|
||||
req.body = body
|
||||
end
|
||||
handle_response(response, "render")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_orders(orders)
|
||||
return nil unless orders
|
||||
|
||||
normalized_orders = {}
|
||||
orders.each do |power, power_orders|
|
||||
# power_ordersがハッシュの場合、値の配列に変換
|
||||
if power_orders.is_a?(Hash)
|
||||
normalized_orders[power.to_s] = power_orders.values
|
||||
elsif power_orders.is_a?(Array)
|
||||
normalized_orders[power.to_s] = power_orders
|
||||
else
|
||||
normalized_orders[power.to_s] = power_orders
|
||||
end
|
||||
end
|
||||
normalized_orders
|
||||
end
|
||||
|
||||
def handle_response(response, endpoint)
|
||||
if response.success?
|
||||
response.body
|
||||
else
|
||||
Rails.logger.error "API Error (#{endpoint}): #{response.status} - #{response.body}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
37
app/services/game_setup_service.rb
Normal file
37
app/services/game_setup_service.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
class GameSetupService
|
||||
def initialize(game, client: GameApiClient.new)
|
||||
@game = game
|
||||
@client = client
|
||||
end
|
||||
|
||||
def setup_initial_turn
|
||||
initial_response = @client.api_game_initial_state
|
||||
|
||||
unless initial_response && initial_response["game_state"]
|
||||
return { success: false, message: "初期状態の取得に失敗しました。" }
|
||||
end
|
||||
|
||||
initial_state = initial_response["game_state"]
|
||||
svg_render = @client.api_render(initial_state)
|
||||
possible_orders = @client.api_calculate_possible_orders(initial_state, by_power: true)
|
||||
|
||||
@game.turns.build(
|
||||
number: 1,
|
||||
game_state: initial_state,
|
||||
possible_orders: possible_orders,
|
||||
phase: initial_state&.dig("name"),
|
||||
svg_date: svg_render,
|
||||
# Initialize svg_orders with NONE (and standard rendering as default)
|
||||
svg_orders: { "NONE" => svg_render }
|
||||
)
|
||||
|
||||
if @game.save
|
||||
{ success: true, message: "ゲームが初期化されました。" }
|
||||
else
|
||||
{ success: false, message: "初期ターンの作成に失敗しました: #{@game.errors.full_messages.join(', ')}" }
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Game setup failed: #{e.message}")
|
||||
{ success: false, message: "予期せぬエラーが発生しました。" }
|
||||
end
|
||||
end
|
||||
87
app/services/order_submission_service.rb
Normal file
87
app/services/order_submission_service.rb
Normal file
@@ -0,0 +1,87 @@
|
||||
class OrderSubmissionService
|
||||
def initialize(turn, user, client: GameApiClient.new)
|
||||
@turn = turn
|
||||
@game = turn.game
|
||||
@user = user
|
||||
@client = client
|
||||
end
|
||||
|
||||
def submit(power:, orders:)
|
||||
# Check permissions
|
||||
unless valid_permission?(power)
|
||||
return { success: false, message: "権限がありません。" }
|
||||
end
|
||||
|
||||
# Handle multiplayer updates (orders_submitted flag)
|
||||
if !@game.solo_mode?
|
||||
participant = @game.participants.find_by(user: @user)
|
||||
participant.update(orders_submitted: true) if participant
|
||||
end
|
||||
|
||||
# Update orders tentatively for validation
|
||||
current_orders = @turn.orders || {}
|
||||
tentative_orders = current_orders.merge(power => orders)
|
||||
|
||||
# Validate
|
||||
validation_result = @client.api_calculate_validate(@turn.game_state, tentative_orders)
|
||||
|
||||
if validation_result.nil?
|
||||
return { success: false, message: "バリデーションサーバーとの通信に失敗しました。" }
|
||||
end
|
||||
|
||||
# Assuming validation_result is an array of error messages or empty if valid
|
||||
# Adjust this based on actual API response structure if known differently.
|
||||
# Common pattern: ["Order A is invalid", "Order B is impossible"]
|
||||
if validation_result.is_a?(Array) && validation_result.any?
|
||||
return { success: false, message: "命令に誤りがあります: #{validation_result.join(', ')}" }
|
||||
end
|
||||
|
||||
# Check if it returns hash with "errors" key?
|
||||
if validation_result.is_a?(Hash) && validation_result["errors"]&.any?
|
||||
return { success: false, message: "命令に誤りがあります: #{validation_result['errors'].join(', ')}" }
|
||||
end
|
||||
|
||||
# If passed validation, update current_orders
|
||||
current_orders[power] = orders
|
||||
|
||||
# Generate SVGs
|
||||
svg_orders_data = @turn.svg_orders || {}
|
||||
|
||||
# None SVG (first time only)
|
||||
unless svg_orders_data["NONE"]
|
||||
none_svg = @client.api_render(@turn.game_state, orders: nil)
|
||||
svg_orders_data["NONE"] = none_svg if none_svg
|
||||
end
|
||||
|
||||
# Power specific SVG
|
||||
power_orders = { power => orders }
|
||||
power_svg = @client.api_render(@turn.game_state, orders: power_orders)
|
||||
svg_orders_data[power] = power_svg if power_svg
|
||||
|
||||
# All SVG
|
||||
all_svg = @client.api_render(@turn.game_state, orders: current_orders)
|
||||
svg_orders_data["ALL"] = all_svg if all_svg
|
||||
|
||||
# Save
|
||||
if @turn.update(orders: current_orders, svg_orders: svg_orders_data)
|
||||
{ success: true, message: "命令を送信しました" }
|
||||
else
|
||||
{ success: false, message: "命令の送信に失敗しました" }
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Order submission failed: #{e.message}")
|
||||
{ success: false, message: "予期せぬエラーが発生しました。" }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_permission?(power)
|
||||
return true if @game.solo_mode?
|
||||
|
||||
participant = @game.participants.find_by(user: @user)
|
||||
return false unless participant
|
||||
|
||||
# Compare case-insensitive
|
||||
participant.power&.upcase == power&.upcase
|
||||
end
|
||||
end
|
||||
135
app/services/turn_processing_service.rb
Normal file
135
app/services/turn_processing_service.rb
Normal file
@@ -0,0 +1,135 @@
|
||||
class TurnProcessingService
|
||||
def initialize(turn, client: GameApiClient.new)
|
||||
@turn = turn
|
||||
@game = turn.game
|
||||
@client = client
|
||||
end
|
||||
|
||||
def process(force: false)
|
||||
# Check for unsubmitted orders in multiplayer
|
||||
if !@game.solo_mode? && !@game.all_orders_submitted?
|
||||
unless force == "true"
|
||||
return { success: false, message: "全プレイヤーの命令入力が完了していません。強制ターン終了ボタンを使用してください。" }
|
||||
end
|
||||
end
|
||||
|
||||
current_orders = @turn.orders || {}
|
||||
|
||||
# 非人間プレイヤーのランダム命令を事前に生成(ターン処理の前に)
|
||||
if @game.auto_order_mode == "random" && !@game.solo_mode?
|
||||
# ゲーム状態から全国のリストを取得(参加者だけでなく全国にランダム命令を生成)
|
||||
all_powers = @turn.game_state&.dig("units")&.keys || []
|
||||
submitted_powers = current_orders.keys.map(&:upcase)
|
||||
|
||||
all_powers.each do |power|
|
||||
# 既に命令が提出済みならスキップ
|
||||
next if submitted_powers.include?(power.upcase)
|
||||
|
||||
auto_orders_response = @client.api_calculate_auto_orders(@turn.game_state, power)
|
||||
|
||||
if auto_orders_response && auto_orders_response["orders"]
|
||||
current_orders[power] = auto_orders_response["orders"]
|
||||
end
|
||||
end
|
||||
|
||||
# 自動生成した命令を現在のターンに保存
|
||||
@turn.update_columns(orders: current_orders) if current_orders.present?
|
||||
end
|
||||
|
||||
# 現在のターンにALL SVG(全プレイヤーの命令を含む画像)を保存
|
||||
# 履歴モードで「このターンでどんな命令が出されたか」を表示するため
|
||||
if current_orders.present?
|
||||
current_svg_orders = @turn.svg_orders || {}
|
||||
all_svg_current = @client.api_render(@turn.game_state, orders: current_orders)
|
||||
if all_svg_current
|
||||
current_svg_orders["ALL"] = all_svg_current
|
||||
@turn.update_columns(svg_orders: current_svg_orders)
|
||||
end
|
||||
end
|
||||
|
||||
# Calculate next state
|
||||
process_response = @client.api_calculate_process(@turn.game_state, current_orders)
|
||||
|
||||
unless process_response
|
||||
return { success: false, message: "ターン処理に失敗しました。" }
|
||||
end
|
||||
|
||||
new_game_state = process_response["game_state"]
|
||||
|
||||
# Transaction to ensure data consistency
|
||||
Game.transaction do
|
||||
# Create next turn foundation
|
||||
possible_orders = @client.api_calculate_possible_orders(new_game_state, by_power: true)
|
||||
svg = @client.api_render(new_game_state, orders: nil)
|
||||
|
||||
new_turn = @game.turns.build(
|
||||
number: @turn.number + 1,
|
||||
game_state: new_game_state,
|
||||
orders: {},
|
||||
phase: new_game_state&.dig("name"),
|
||||
possible_orders: possible_orders,
|
||||
svg_date: svg,
|
||||
svg_orders: { "NONE" => svg }
|
||||
)
|
||||
|
||||
# Save new turn
|
||||
if new_turn.save
|
||||
# Reset orders_submitted flag
|
||||
@game.participants.update_all(orders_submitted: false) unless @game.solo_mode?
|
||||
|
||||
# ハウスルール: 勝利条件判定
|
||||
result = check_victory_conditions(new_turn, new_game_state)
|
||||
return result if result
|
||||
|
||||
{ success: true, message: "ターンを終了し、次のフェーズへ進みました。" }
|
||||
else
|
||||
{ success: false, message: "次のターンの作成に失敗しました: #{new_turn.errors.full_messages.join(', ')}" }
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Turn processing failed: #{e.message}")
|
||||
Rails.logger.error(e.backtrace.join("\n"))
|
||||
{ success: false, message: "予期せぬエラーが発生しました。" }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_victory_conditions(new_turn, new_game_state)
|
||||
centers = new_game_state&.dig("centers") || {}
|
||||
|
||||
# 1. ソロ勝利判定
|
||||
if @game.solo_victory?(new_game_state)
|
||||
winner = @game.solo_victory_power(new_game_state)
|
||||
finish_game(new_turn, "solo", [ winner ])
|
||||
return { success: true, message: "#{winner} が #{@game.victory_sc_count} SC を獲得し、ソロ勝利しました!" }
|
||||
end
|
||||
|
||||
# 2. 年数制限判定
|
||||
if @game.year_limit_reached?(new_turn.phase)
|
||||
# SC数最多の国が勝者、同数なら引き分け
|
||||
max_sc = centers.values.map(&:size).max
|
||||
winners = centers.select { |_power, scs| scs.size == max_sc }.keys
|
||||
finish_game(new_turn, winners.size == 1 ? "year_limit_solo" : "year_limit_draw", winners)
|
||||
result_type = winners.size == 1 ? "#{winners.first} の勝利" : "#{winners.join(', ')} の引き分け"
|
||||
return { success: true, message: "年数制限(#{@game.year_limit}年)に達しました。#{result_type}です。" }
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def finish_game(last_turn, result_type, winners)
|
||||
# 完了ターンを作成
|
||||
Turn.create!(
|
||||
game: @game,
|
||||
number: last_turn.number + 1,
|
||||
game_state: last_turn.game_state,
|
||||
orders: {},
|
||||
possible_orders: {},
|
||||
phase: "COMPLETED",
|
||||
svg_date: last_turn.svg_date,
|
||||
svg_orders: last_turn.svg_orders
|
||||
)
|
||||
@game.update!(status: "finished")
|
||||
Rails.logger.info("Game #{@game.id} finished: #{result_type}, winners: #{winners.join(', ')}")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user