MOE3: 移動失敗後の維持への移動支援の無効

spec/models/main_phase_spec.rb

マニュアルの Diagram12、同等戦力のスタンドオフで移動に失敗した German A Mun-Sil が支援付きの Austrian A Boh-Mun に撃退される処理を実装する*1

これは、移動支援は移動に失敗したユニットの維持には効果がないことの例示である。

f:id:asagix:20130919100326p:plain:w200

describe MainPhase do
  describe "#resolve_orders" do
    let(:phase) { MainPhase.create }
    let(:resolved_orders) { phase.resolve_orders }

    #(略)

    shared_context "Diagram12 移動失敗後の維持には移動支援は適用されない", diagram: 12 do
      let(:army_boh) { FactoryGirl.create(:army, :a, :boh, phase: phase) }
      let(:army_tyr) { FactoryGirl.create(:army, :a, :tyr, phase: phase) }
      let(:army_mun) { FactoryGirl.create(:army, :g, :mun, phase: phase) }
      let(:army_ber) { FactoryGirl.create(:army, :g, :ber, phase: phase) }
      let(:army_war) { FactoryGirl.create(:army, :r, :war, phase: phase) }
      let(:army_pru) { FactoryGirl.create(:army, :r, :pru, phase: phase) }

      let(:prov_mun) { army_mun.province }
      let(:prov_sil) { Province.find_by(code: "sil") }

      let!(:move_boh_mun) { FactoryGirl.create(:move_order, unit: army_boh, destination: prov_mun) }
      let!(:supp_tyr_boh) { FactoryGirl.create(:support_order, unit: army_tyr, target: move_boh_mun) }
      let!(:move_mun_sil) { FactoryGirl.create(:move_order, unit: army_mun, destination: prov_sil) }
      let!(:supp_ber_mun) { FactoryGirl.create(:support_order, unit: army_ber, target: move_mun_sil) }
      let!(:move_war_sil) { FactoryGirl.create(:move_order, unit: army_war, destination: prov_sil) }
      let!(:supp_pru_war) { FactoryGirl.create(:support_order, unit: army_pru, target: move_war_sil) }
    end

    context "Diagram12", diagram: 12 do
      subject { resolved_orders }
      example { expect(subject.find(move_boh_mun).status).to eq OrderStatus::SUCCESS}
      example { expect(subject.find(supp_tyr_boh).status).to eq OrderStatus::SATISFIED }
      example { expect(subject.find(move_mun_sil).status).to eq OrderStatus::DISLODGED}
      example { expect(subject.find(supp_ber_mun).status).to eq OrderStatus::SATISFIED }
      example { expect(subject.find(move_war_sil).status).to eq OrderStatus::STANDOFF}
      example { expect(subject.find(supp_pru_war).status).to eq OrderStatus::SATISFIED }
    end
  end
end

実行すると、German A Mun-Sil が STANDOFF で Austrian A Boh-Mun が FAILURE。

さて。失敗した移動命令に支援が効いているのか、失敗した移動命令への攻撃処理が抜けているのか。

MainPhase#resolve_attacks メソッド

スタンドオフを処理する MainPhase#resolve_conflicts の次に実行されるのがこちら。

ざっと見た感じ、処理そのものに問題はなさそうだが。

  # 維持ユニットへの攻撃
  def resolve_attacks
    moves = @orders.select{|o| o.move?}
    moves.each do |move|
      hold = @orders.select{|o| o.hold? && o.province == move.dst}[0]
      unless hold
        unexecuted_move = @orders.select{|o| o.move? && o.src == move.dst}[0]
        move.success! unless unexecuted_move
        next
      end

      move_supports = @orders.select{|o| o.support? && o.target?(move)}.size
      hold_supports = @orders.select{|o| o.support? && o.target?(hold)}.size

      if move_supports > hold_supports
        move.success!
        hold.dislodged!
      else
        move.failure!
        hold.success! if hold.unexecuted?
      end
    end
  end

これはあれだ、SupportOrder#target? で、支援対象の移動命令が失敗済みなら false を返すようにすれば良いはずだ。きっと。

SupportOrder#target? メソッド

before。

  def target?(order)
    return false if self.target.unit != order.unit
    return false if self.target.dst != order.dst
    true
  end

after。

  def target?(order)
    return false if self.target.unit != order.unit
    return false if self.target.dst != order.dst
    return false if self.target.move? != order.move?
    true
  end

MoveOrder#move? は UNEXECUTED の時だけ true を返すことを思い出して欲しい。

一方で支援命令が保有する標本命令のステータスは UNEXECUTED のまま。

つまり標本命令が MoveOrder なら move? は常に true を返すから、これで move?false を返す失敗済みの移動命令は支援対象として認識されなくなるわけだ。

……うーむ、直観的ではないな。

とにかくテスト

実行。オールグリーン。完了。

MoveOrder#move?SupportOrder#target? の複合技はちょっとトリッキーなので、どこかで見直ししないと未来の自分が理解できなくなりそうな不安がある。注意。

*1:ユニット数が多くて全部書くのめんどい。

MOE3: 同等戦力の維持ユニットへの攻撃

spec/models/main_phase_spec.rb

マニュアルの Diagram11、French F Gol-TynFrench F Wes S F Gol-TynItalian F Tyn-HoldsItalian F Rom S F Tyn-Holds を実装する。

f:id:asagix:20130919100325p:plain:w200

繰り返しになるが、MOE3 の地名は jDip 準拠なので "Gol" は "Lyo" となる。

それと、現行の MOE2 もそうだが、支援命令は対象が移動かそれ以外かのみを区別する都合により、上の例であれば F Rom S F Tyn-Holds は実際の画面では F Rom S F Tyn と表示される予定。

では、テストを書こう。

describe MainPhase do
  describe "#resolve_orders" do
    let(:phase) { MainPhase.create }
    let(:resolved_orders) { phase.resolve_orders }

    #(略)

    shared_context "Diagram11 同等戦力の維持ユニットへの攻撃", diagram: 11 do
      let(:fleet_lyo) { FactoryGirl.create(:fleet, :f, :lyo, phase: phase) }
      let(:fleet_wes) { FactoryGirl.create(:fleet, :f, :wes, phase: phase) }
      let(:fleet_tyn) { FactoryGirl.create(:fleet, :i, :tyn, phase: phase) }
      let(:fleet_rom) { FactoryGirl.create(:fleet, :i, :rom, phase: phase) }

      let(:prov_tyn) { fleet_tyn.province }

      let!(:move_lyo_tyn) { FactoryGirl.create(:move_order, unit: fleet_lyo, destination: prov_tyn) }
      let!(:supp_wes_lyo) { FactoryGirl.create(:support_order, unit: fleet_wes, target: move_lyo_tyn) }
      let!(:hold_tyn) { FactoryGirl.create(:hold_order, unit: fleet_tyn) }
      let!(:supp_rom_tyn) { FactoryGirl.create(:support_order, unit: fleet_rom, target: hold_tyn) }
    end

    context "Diagram11", diagram: 11 do
      subject { resolved_orders }
      example { expect(subject.find(move_lyo_tyn).status).to eq OrderStatus::FAILURE}
      example { expect(subject.find(supp_wes_lyo).status).to eq OrderStatus::SATISFIED }
      example { expect(subject.find(hold_tyn).status).to eq OrderStatus::SUCCESS}
      example { expect(subject.find(supp_rom_tyn).status).to eq OrderStatus::SATISFIED }
    end
  end
end

問題なく成功。

MOE3: 行軍解決処理のリファクタリング

行軍解決終了時の未処理命令の一律成功処理

そのうちなんとかすると言ったな。あれは嘘だ。

やはり行軍解決処理が終了する時点で未処理命令が残っているのは好ましくない。

気になるのでさっさと片付けてしまおう。

エラー状況の確認

まず MainPhase#cleanup の一律成功処理を外してテストを実行してみる。

  def cleanup
    @orders.each do |order|
      #order.success! if order.unexecuted? # 暫定コメントアウト
      order.save!
    end
  end

すると当然、French F Gol-Tyn が UNEXECUTED のままなので、前回最後に追加したテストがエラーになる。

MainPhase#resolve_attack メソッドの修正

処理の順番的に resolve_conflicts の次に来るのが resolve_attack である。

現状、移動先に維持命令がない場合はその移動命令の成否判定をスキップする仕様だが、これを変更する。移動先に維持命令がなければその移動は成功でいいじゃないか。

というわけで Before。

  # 維持ユニットへの攻撃
  def resolve_attack
    moves = @orders.select{|o| o.move?}
    moves.each do |move|
      hold = @orders.select{|o| o.hold? && o.province == move.dst}[0]
      next unless hold

      move_supports = @orders.select{|o| o.support? && o.target?(move)}.size
      hold_supports = @orders.select{|o| o.support? && o.target?(hold)}.size

      if move_supports > hold_supports
        move.success!
        hold.dislodged!
      else
        move.failure!
        hold.success! if hold.unexecuted?
      end
    end
  end

そして after。

  # 維持ユニットへの攻撃
  def resolve_attack
    moves = @orders.select{|o| o.move?}
    moves.each do |move|
      hold = @orders.select{|o| o.hold? && o.province == move.dst}[0]
      unless hold
        move.success!
        next
      end

      move_supports = @orders.select{|o| o.support? && o.target?(move)}.size
      hold_supports = @orders.select{|o| o.support? && o.target?(hold)}.size

      if move_supports > hold_supports
        move.success!
        hold.dislodged!
      else
        move.failure!
        hold.success! if hold.unexecuted?
      end
    end
  end

おや、Diagram6 のテストがエラーになってしまった。

交換移動の 2 命令が SUCCESS になっている。

MainPhase#resolve_attack メソッドの追加修正

成功確定条件が「移動先に維持命令がない」だけだと、resolve_rotation の前に交換移動命令がそれぞれ SUCCESS で確定してしまうのが問題。

条件に「移動先に未解決の移動命令がない」も追加する。

  # 維持ユニットへの攻撃
  def resolve_attack
    moves = @orders.select{|o| o.move?}
    moves.each do |move|
      hold = @orders.select{|o| o.hold? && o.province == move.dst}[0]
      unless hold
        unexecuted_move = @orders.select{|o| o.move? && o.src == move.dst}[0]
        move.success! unless unexecuted_move
        next
      end

      move_supports = @orders.select{|o| o.support? && o.target?(move)}.size
      hold_supports = @orders.select{|o| o.support? && o.target?(hold)}.size

      if move_supports > hold_supports
        move.success!
        hold.dislodged!
      else
        move.failure!
        hold.success! if hold.unexecuted?
      end
    end
  end

テスト。はい OK。

ちょっと泥臭い書き方になったので気が向いたら綺麗にしておこう。

こっそり修正

MainPhase#resolve_moveMainPhase#resolve_attack のメソッド名を、それぞれ resolve_movesresolve_attacks に修正。

主に気分の問題。

MainPhase#resolve_holds メソッド

孤独な維持命令の成功確定処理も忘れないうちに書くだけ書いてしまおう。実行タイミングは apply_supports の次でいいか。

本当はテストを先に書くべきなのだが時には勢いも大切だよね。

  def resolve_orders
    setup
    apply_supports
    resolve_holds
    resolve_moves
    resolve_conflicts
    resolve_attacks
    resolve_pileup
    resolve_rotation
    cleanup
    orders(true)
  end

  #(略)

  # 干渉を受けない維持
  def resolve_holds
    holds = @orders.select{|o| o.hold?}
    holds.each do |hold|
      moves = @orders.select{|o| o.move? && o.dst == hold.province}
      next if moves.size != 0
      next unless hold.unexecuted?
      hold.success!
    end
  end

hold?true を返すすべての未処理命令を SUCCESS にしてしまうので、輸送成否判定を実装する時は resolve_holds の前に入れる必要があることだけ覚えておこう。

そしてテスト。もちろん問題なし。

後始末

お終いに MainPhase#cleanup を綺麗にして完了。お疲れ。

  def cleanup
    @orders.each do |order|
      order.save!
    end
  end

Redmine を Nginx + Unicorn で動かす

前回までのあらすじ

Nginx を Apache のリバースプロキシに設定した。

今日の目標

現在 Apache + Passenger で動いている Rails アプリの Redmine を Nginx + Unicorn に切り替えてみよう。

Redmine は /var/www/redmine に設置されている。

Gem の追加

Redmine の gem に unicorn を追加する。

もしRedmine本体は使用しないgem(例: mongrel, fcgi)もロードしたい場合、 Gemfile.local というファイルをRedmineのディレクトリに作成してください。 Redmineのインストール — Redmine Guide 日本語訳

ということなので Gemfile.local を作成した。

# Gemfile.local
gem "unicorn"

インストール。

$ bundle install

Unicorn の設定

Redmine の config ディレクトリに unicorn.rb というファイルを作成した。

worker_processes 1

timeout 60

listen File.expand_path("tmp/sockets/unicorn.sock", ENV['RAILS_ROOT'])

pid File.expand_path("tmp/pids/unicorn.pid", ENV['RAILS_ROOT'])

stdout_path File.expand_path("log/unicorn.stdout.log", ENV['RAILS_ROOT'])
stderr_path File.expand_path("log/unicorn.stderr.log", ENV['RAILS_ROOT'])

preload_app true
GC.respond_to?(:copy_on_write_friendly=) and GC.copy_on_write_friendly = true

before_fork do |server, worker|
  # この設定はpreload_app trueの場合に必須
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!

  # USR2シグナルで旧プロセスを終了させる
  old_pid = "#{server.config[:pid]}.oldbin"
  if old_pid != server.pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end
end

after_fork do |server, worker|
  # この設定はpreload_app trueの場合に必須
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

とりあえず socket のパス。これ大事。

tmp/sockets/unicorn.sock

pid のパス。これも大事。

tmp/pids/unicorn.pid

Unicorn 起動

え、Unicorn て Apache とか Nginx とかとは別口で起動しておかないといけないの?

後で自動化できないか調べなきゃ。

$ bundle exec unicorn_rails -c config/unicorn.rb -D -E production

コマンドのオプションは -c で設定ファイル指定、-E で動作モード指定、-D がデーモン化。

終了の仕方は力ずく。

$ kill -quit 'cat /var/www/redmine/tmp/pids/unicorn.pid'

Nginx の設定

/etc/nginx/sites-available/ に設定ファイルを作成して /etc/nginx/sites-enabled/ からリンクを張る。

upstream redmine {
  server unix:/var/www/redmine/tmp/sockets/unicorn.sock;
}

server {
  listen 80;
  server_name redmine.tepidworks.jp;

  location / {
    root /var/www/redmine/public;

    if ( -f $request_filename ) { break; }

    proxy_set_header X-Real-IP  $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_pass http://redmine;
  }
}

if ( -f $request_filename ) { break; } があるので、静的ファイルへのアクセスは Rails を通さないで Nginx が直接返す。*1

Nginx 再起動

$ sudo service nginx restart

無事アクセスできた。

*1:少なくとも、確認した限り静的ファイルへのアクセスでは Rails がログを吐かなかったので Unicorn に処理は渡っていないはず。

Apache のリバースプロキシとして Nginx を導入する

参考

NginxとApache - Qiita [キータ]

環境

Debian 7.1
Apache 2.2.22
Nginx 1.2.1

Nginx

Rails 関連の情報を漁っていて、前々から「Nginx + Unicorn が良いらしい」と聞いていたので興味はあったのだが、Nginx については「Apache より軽量な HTTP サーバー」程度の認識しかなかった。

ところが今さらながらに Nginx と Apache の共存が可能と知り、「Nginx + Unicorn + Rails」な環境に向けて、ひとまず Apache のリバースプロキシとして Nginx を導入することにした。

実は再挑戦だった

基本的に参考サイトの見よう見まねでの作業となったが、改めてサーバーの環境を確認したところ、Nginx はなぜかインストール済み。

そういえば半年ほど前に入れるだけ入れて良くわからないまま放置していたのだった。

その時に変な風に設定をいじっていたら嫌だなあと思いつつ、面倒くさいので再インストールはしなかったが、結果的に問題はなかったので良しとしておこう。

mod_rpaf

良く分からないのが mod_rpaf だが、どうやら Nginx が受けたリクエストを Apache に渡す時点で、Apache から見ると全部 localhost からのアクセスに見えてしまって実際のアクセス元が分からなくなってしまう問題への対処として必要らしい。

apt で一発。apache のモジュール設定の変更から再起動までやってくれた。素晴らしい。

# aptitude install libapache2-mod-rpaf 

Nginx の設定

Nginx がポート 80 で受けたリクエストを 8000 へリダイレクトさせるための設定ファイルを /etc/nginx/sites-available/ に作成して /etc/nginx/sites-enabled/ からリンクを張った。

/etc/nginx/sites-available/default へのリンクは使わないので削除。

このあたりの構成は debian 系ユーザであれば apache の設定と同じなので馴染みやすいと思う。 a2ensitea2dissite に相当するコマンドがあればなお良かったが贅沢は言うまい。

apache の設定

/etc/apache2/sites-available/ にある VirtualHost 別の設定ファイルのポートをすべて 8000 に変更、/etc/apache2/ports.conf の NameVirtualHost を *:8000 に、Listen を localhost:8000 に変更した。

Listen の設定で localhost を指定したのは、外部からのポート 8000 へのアクセスを弾くためだ。

再起動

Nginx と Apache を再起動して、無事にアクセスできることを確認。

めでたしめでたし。

MOE3: 支援付きスタンドオフの解決

spec/models/main_phase_spec.rb

さくさく行こう。

マニュアルの Diagram10、French F Gol-TynFrench F Wes S F Gol-TynItalian F Nap-TynItalian F Rom S F Nap-Tyn の支援付きスタンドオフを実装する。

f:id:asagix:20130919100324p:plain:w200

ちなみに MOE3 の地名は jDip 準拠なので、"Gulf of Lyon" の略称は "Gol" ではなく "Lyo" とする。

describe MainPhase do
  describe "#resolve_orders" do
    let(:phase) { MainPhase.create }
    let(:resolved_orders) { phase.resolve_orders }

    #(略)

    shared_context "Diagram10 支援付きスタンドオフ", diagram: 10 do
      let(:fleet_lyo) { FactoryGirl.create(:fleet, :f, :lyo, phase: phase) }
      let(:fleet_wes) { FactoryGirl.create(:fleet, :f, :wes, phase: phase) }
      let(:fleet_nap) { FactoryGirl.create(:fleet, :i, :nap, phase: phase) }
      let(:fleet_rom) { FactoryGirl.create(:fleet, :i, :rom, phase: phase) }

      let(:prov_tyn) { Province.find_by(code: "tyn") }

      let!(:move_lyo_tyn) { FactoryGirl.create(:move_order, unit: fleet_lyo, destination: prov_tyn) }
      let!(:supp_wes_lyo) { FactoryGirl.create(:support_order, unit: fleet_wes, target: move_lyo_tyn) }
      let!(:move_nap_tyn) { FactoryGirl.create(:move_order, unit: fleet_nap, destination: prov_tyn) }
      let!(:supp_rom_nap) { FactoryGirl.create(:support_order, unit: fleet_rom, target: move_nap_tyn) }
    end

    context "Diagram10", diagram: 10 do
      subject { resolved_orders }
      example { expect(subject.find(move_lyo_tyn).status).to eq OrderStatus::STANDOFF }
      example { expect(subject.find(supp_wes_lyo).status).to eq OrderStatus::SATISFIED }
      example { expect(subject.find(move_nap_tyn).status).to eq OrderStatus::STANDOFF }
      example { expect(subject.find(supp_rom_nap).status).to eq OrderStatus::SATISFIED }
    end
  end
end

MainPhase#resolve_standoff メソッド

既存のコードに手を入れることなくテストが通ってしまった。

戦力が同等なので当たり前。

  # スタンドオフ
  def resolve_standoff
    moves = @orders.select{|o| o.move?}
    moves.collect{|m| m.dst}.uniq.each do |dst|
      conflicts = moves.select{|m| m.dst == dst}
      next if conflicts.size == 1

      conflicts.each do |move|
        move.standoff!
      end
    end
  end

spec/models/main_phase_spec.rb

テストを追加する。

Diagram10 を改変し、Italian F Rom S F Nap-Tyn を外す。

これで French F Gol-TynSUCCESSItalian F Nap-TynFAILURE になれば成功。

describe MainPhase do
  describe "#resolve_orders" do
    let(:phase) { MainPhase.create }
    let(:resolved_orders) { phase.resolve_orders }

    #(略)

    shared_context "Diagram10.1 戦力差のある同一地域への移動", diagram: 10.1 do
      let(:fleet_lyo) { FactoryGirl.create(:fleet, :f, :lyo, phase: phase) }
      let(:fleet_wes) { FactoryGirl.create(:fleet, :f, :wes, phase: phase) }
      let(:fleet_nap) { FactoryGirl.create(:fleet, :i, :nap, phase: phase) }

      let(:prov_tyn) { Province.find_by(code: "tyn") }

      let!(:move_lyo_tyn) { FactoryGirl.create(:move_order, unit: fleet_lyo, destination: prov_tyn) }
      let!(:supp_wes_lyo) { FactoryGirl.create(:support_order, unit: fleet_wes, target: move_lyo_tyn) }
      let!(:move_nap_tyn) { FactoryGirl.create(:move_order, unit: fleet_nap, destination: prov_tyn) }
    end

    context "Diagram10.1", diagram: 10.1 do
      subject { resolved_orders }
      example { expect(subject.find(move_lyo_tyn).status).to eq OrderStatus::SUCCESS }
      example { expect(subject.find(move_nap_tyn).status).to eq OrderStatus::FAILURE }
    end
  end
end

そのままテストを実行すると、もちろん NG。

さあ、MainPhase#resolve_standoff を修正しよう。

メソッド名も変更した方が良さそうだな。resolve_conflicts でいいか。

resolve_standoff 改め reslove_conflicts メソッド

  # 同一地域への移動
  def resolve_conflicts
    moves = @orders.select{|o| o.move?}
    moves.collect{|m| m.dst}.uniq.each do |dst|
      conflicts = moves.select{|m| m.dst == dst}
      next if conflicts.size == 1

      # 戦力抽出
      supported_moves = {}
      conflicts.each do |move|
        supported_moves[move] = @orders.select{|o| o.support? && o.target?(move)}.size
      end

      # 最大戦力取得
      max_supports = supported_moves.values.sort.last

      if supported_moves.select{|k,v| v == max_supports}.size != 1
        # 最大戦力が同等の移動が複数ある場合は全てスタンドオフ
        conflicts.each do |move|
          move.standoff!
        end
        # スタンドオフ地域に維持ユニットがいたら維持成功とする
        hold = @orders.select{|o| o.hold? && o.province == dst}[0]
        if hold && hold.unexecuted?
          hold.success!
        end
      else
        # 最大戦力の移動以外は失敗
        conflicts.each do |move|
          next if move == supported_moves.key(max_supports)
          move.failure!
        end
      end
    end
  end

移動先に維持ユニットがいたら別途攻撃成否判定が必要になるので、このタイミングでは最大戦力の移動命令を SUCCESS にできない。

「スタンドオフ地域に維持ユニットがいたら~」は少々気の回し過ぎかもしれないが、どうせいずれは必要になるので入れておく。

MainPhase#cleanup メソッド

このままだと French F Gol-TynUNEXECUTED のまま処理が終了してしまうので、MainPhase#cleanup で未処理命令を一律 SUCCESS にする処理を入れてお茶を濁す。どこからも干渉されない単品の維持命令などもこれで成功確定できる。

  def cleanup
    @orders.each do |order|
      order.success! if order.unexecuted?
      order.save!
    end
  end

だが、美しくない。そのうちなんとかする。

MOE3: 支援適用処理

spec/models/main_phase_spec.rb

再掲。

describe MainPhase do
  describe "#resolve_orders" do
    let(:phase) { MainPhase.create }
    let(:resolved_orders) { phase.resolve_orders }

    #(略)

    shared_context "Diagram8 移動支援", diagram: 8 do
      let(:army_mar) { FactoryGirl.create(:army, :f, :mar, phase: phase) }
      let(:army_gas) { FactoryGirl.create(:army, :f, :gas, phase: phase) }
      let(:army_bur) { FactoryGirl.create(:army, :g, :bur, phase: phase) }

      let(:prov_bur) { army_bur.province }

      let!(:move_mar_bur) { FactoryGirl.create(:move_order, unit: army_mar, destination: prov_bur) }
      let!(:supp_gas_mar) { FactoryGirl.create(:support_order, unit: army_gas, target: move_mar_bur) }
      let!(:hold_bur) { FactoryGirl.create(:hold_order, unit: army_bur) }
    end

    context "Diagram8", diagram: 8 do
      subject { resolved_orders }
      example { expect(subject.find(move_mar_bur).status).to eq OrderStatus::SUCCESS }
      example { expect(subject.find(supp_gas_mar).status).to eq OrderStatus::SATISFIED }
      example { expect(subject.find(hold_bur).status).to eq OrderStatus::DISLODGED }
    end
  end
end

MainPhase#apply_supports メソッド

前回のテストが通っていないので、新規のテストは不要。

例によってメソッド名に悩むが、わりとどうでもいいことなのでこのまま行く。

対象が存在すれば SATISFIED、存在しなければ INVALID にするだけの簡単なお仕事です。

class MainPhase < Phase
  def resolve_orders
    setup
    apply_supports
    resolve_move
    resolve_standoff
    resolve_attack
    resolve_pileup
    resolve_rotation
    cleanup
    orders(true)
  end

  #(略)

  # 支援適用
  def apply_supports
    supports = @orders.select{|o| o.support?}
    supports.each do |support|
      target = @orders.select{|o| support.target?(o)}[0]
      if target
        support.satisfied!
      else
        support.invalid!
      end
    end
  end

  #(略)
end

省略しているけど Order#satisfied!Order#invalid! は当然実装済み。

これで前回書いたテストは無事通ってめでたしめでたし。

今後の展望

Diagram9 は移動支援の前提条件確認*1なので省略。

f:id:asagix:20130917163157p:plain:w200

Diagram10 から 14 はこれまでの行軍処理に支援と戦力を盛り込んでいく形になるはず。

支援のカットについては Diagram15 までお預け。

*1:支援する軍が支援対象の移動先に移動可能であること。