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

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:支援する軍が支援対象の移動先に移動可能であること。

MOE3: 支援付き攻撃の解決

spec/models/main_phase_spec.rb

いよいよ本番といった感じ。

マニュアルの Diagram8、French A Mar–BurFrench A Gas S A Mar-BurGerman A Bur-Holds の支援、攻撃、敗退処理を実装する。

f:id:asagix:20130917115506p:plain:w200

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#resolve_attack メソッド

処理の順番はスタンドオフ解決後。

支援のカットや自軍攻撃支援の無効化などは後回しで、まずはシンプルに。

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

  #(略)

  # 維持ユニットへの攻撃
  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

  #(略)
end

色々足りてないので、当然エラーになる。

SupportOrder#support? メソッド

とりあえず無条件で true

後々、カットまたは撃退されたら false を返すようになるはず。

class SupportOrder < Order
  def support?
    true
  end
end

もちろん Order#support? は常に false を返すようにしておく。

SupportOrder#target? メソッド

こちらも Order#target?false を返すようにしておく。

ついでに MoveOrder かどうかを意識せずに dst を呼べるように、常に nil を返す Order#dst もこっそり追加。ステータス変更用の Order#dislodged! もね。

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

とりあえずここまで。

現状整理

SupportOrder のステータスを変更する処理を入れていないので、このままでは通らないテストがある。

次回、MainPhase#apply_supports メソッドを実装しよう。

MOE3: 循環移動の解決

spec/models/main_phase_spec.rb

テスト。

マニュアルの Diagram6、English F Nth–HolFrench A Hol–BelFrench F Bel-Nth の循環移動処理を実装する。

f:id:asagix:20130912114127p:plain:w200

ここまでの実装だと、それぞれ移動先に未解決の移動命令があるため成否が確定しない。

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

    #(略)

    shared_context "Diagram7 循環移動", diagram: 7 do
      let(:army_hol) { FactoryGirl.create(:army, :e, :hol, phase: phase) }
      let(:fleet_bel) { FactoryGirl.create(:fleet, :e, :bel, phase: phase) }
      let(:fleet_nth) { FactoryGirl.create(:fleet, :f, :nth, phase: phase) }

      let(:prov_hol) { army_hol.province }
      let(:prov_bel) { fleet_bel.province }
      let(:prov_nth) { fleet_nth.province }

      let!(:move_hol_bel) { FactoryGirl.create(:move_order, unit: army_hol, destination: prov_bel) }
      let!(:move_bel_nth) { FactoryGirl.create(:move_order, unit: fleet_bel, destination: prov_nth) }
      let!(:move_nth_hol) { FactoryGirl.create(:move_order, unit: fleet_nth, destination: prov_hol) }
    end

    context "Diagram7", diagram: 7 do
      subject { resolved_orders }
      example { expect(subject.find(move_hol_bel).status).to eq OrderStatus::SUCCESS }
      example { expect(subject.find(move_bel_nth).status).to eq OrderStatus::SUCCESS }
      example { expect(subject.find(move_nth_hol).status).to eq OrderStatus::SUCCESS }
    end
  end
end

MainPhase#resolve_rotation メソッド

突き詰めれば交換移動も一種の循環移動ということで、こちらに混ぜてしまった。

それにより MainPhase#resolve_exchange はお役御免で削除。

MainPhase#resolve_rotation の仕様

  • 移動命令の移動先が連鎖する到達点を調査する。
  • 他の移動命令処理の過程で未処理の移動命令が変化する可能性に留意。
  • 2軍による循環は交換移動なのでどちらも失敗。
  • 移動先の連鎖が循環していなければ何もしない。
  • 移動先の連鎖が循環していたら全て成功。
class MainPhase < Phase
  #(略)

  # 交換・循環移動
  def resolve_rotation
    moves = @orders.select{|o| o.move?}
    moves.each do |move1|
      next unless move1.move?

      move2 = moves.select{|m| m.move? && m.src == move1.dst}[0]
      next unless move2

      if move2.dst == move1.src
        move1.failure!
        move2.failure!
        next
      end

      check_rotate(moves, move1, move2)
    end
  end

  def check_rotate(moves, move1, move2)
    move3 = moves.select{|m| m.src == move2.dst}[0]
    return false unless move3

    if move3 == move1
      move1.success!
      move2.success!
      return true
    end

    if check_rotate(moves, move1, move3)
      move2.success!
    end
  end

  #(略)
end

循環の判定は MainPhase#check_rotate に切り出して再帰呼び出し。

movesmove1 は固定で、move2 を先に進めつつ移動連鎖の先端を探す。

現状の課題

MainPhase#check_rotate は循環が確定したら再帰呼び出しを逆に辿りながら成功確定していくところが若干分かり辛いかもしれない。

また、循環していない移動命令の連鎖では無駄な走査が発生する場合があるので、前段階の処理も含めて一度見直す必要がありそうだ。