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 は循環が確定したら再帰呼び出しを逆に辿りながら成功確定していくところが若干分かり辛いかもしれない。

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

MOE3: 玉突き衝突処理のリファクタリング

MoveOrder#resolve_pileup メソッド修正

やっぱり無限ループは怖いのでやっつけ修正。

失敗が確定した MoveOrder@orders.select{|o| o.move?} から外れるので、全ての MoveOrder の処理が完了するか、処理しきれない分が確定するまでループで回す。

class MainPhase < Phase
  #(略)

  # 玉突き衝突
  def resolve_pileup
    last_moves_size = 0
    loop do
      moves = @orders.select{|o| o.move?}
      break if moves.size == 0
      break if moves.size == last_moves_size
      last_moves_size = moves.size

      moves.collect{|m| m.dst}.uniq.each do |dst|
        hold = @orders.select{|o| o.hold? && o.province == dst}[0]
        next unless hold

        moves.select{|m| m.dst == dst}.each do |m1|
          m1.failure!
        end
      end
    end
  end

  #(略)
end

うーん、なんか気に入らない。もっと綺麗に書けるはず。

MainPhase#resolve_piliup メソッド再修正

再帰で書き直してみた。

class MainPhase < Phase
  #(略)

  # 玉突き衝突
  def resolve_pileup(last_moves_size = 0)
    moves = @orders.select{|o| o.move?}
    return if moves.size == 0
    return if moves.size == last_moves_size

    moves.each do |move|
      hold = @orders.select{|o| o.hold? && o.province == move.dst}[0]
      move.failure! if hold
    end
    resolve_pileup(moves.size)
  end

  #(略)
end

一般的に、ループの方が再帰よりもリソースや速度面で有利と言われている。

それでも最大 34 回程度のループなら誤差の範囲だろうし、ソースの読みやすさで言えばやはり再帰の方が上なのでこれで良しとする。

MOE3: 交換移動の解決

spec/models/main_phase_spec.rb

テスト。

マニュアルの Diagram6、German F Ber–PruGerman A Pru–Ber の交換移動禁止処理を実装する。

f:id:asagix:20130912114126p:plain:w200

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

    #(略)

    shared_context "Diagram6 交換", diagram: 6 do
      let(:fleet_ber) { FactoryGirl.create(:fleet, :g, :ber, phase: phase) }
      let(:army_pru) { FactoryGirl.create(:army, :g, :pru, phase: phase) }

      let(:prov_ber) { fleet_ber.province }
      let(:prov_pru) { army_pru.province }

      let!(:move_ber_pru) { FactoryGirl.create(:move_order, unit: fleet_ber, destination: prov_pru) }
      let!(:move_pru_ber) { FactoryGirl.create(:move_order, unit: army_pru, destination: prov_ber) }
    end

    context "Diagram6", diagram: 6 do
      subject { resolved_orders }
      example { expect(subject.find(move_ber_pru).status).to eq OrderStatus::FAILURE }
      example { expect(subject.find(move_pru_ber).status).to eq OrderStatus::FAILURE }
    end
  end
end

MainPhase#resolve_exchange メソッド

まずは効率度外視で思いついたまま、テストを通過するためだけの処理を書いてみた。

class MainPhase < Phase
  #(略)

  # 交換
  def resolve_exchange
    moves = @orders.select{|o| o.move?}
    moves.collect{|m| m.dst}.uniq.each do |dst|
      move1 = moves.select{|m| m.dst == dst && m.unexecuted?}[0]
      next unless move1
      move2 = moves.select{|m| m.src == move1.dst && m.dst == move1.src}[0]
      next unless move2

      move1.failure!
      move2.failure!
    end
  end

  #(略)
end

ここでリファクタリングを実施しても良いが、少し思うところがあるので今はこのままにしておく。

MOE3: 玉突き衝突の解決

spec/models/main_phase_spec.rb

テストを書く。

マニュアルの Diagram5、German A Ber–PruGerman A Kie–Ber の移動失敗の連鎖処理を実装する。

f:id:asagix:20130912114125p:plain:w200

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

    #(略)

    shared_context "Diagram5 玉突き衝突", diagram: 5 do
      let(:army_pru) { FactoryGirl.create(:army, :r, :pru, phase: phase) }
      let(:army_ber) { FactoryGirl.create(:army, :g, :ber, phase: phase) }
      let(:army_kie) { FactoryGirl.create(:army, :g, :kie, phase: phase) }
      let(:prov_pru) { army_pru.province }
      let(:prov_ber) { army_ber.province }

      let!(:hold_pru) { FactoryGirl.create(:hold_order, unit: army_pru) }
      let!(:move_ber_pru) { FactoryGirl.create(:move_order, unit: army_ber, destination: prov_pru) }
      let!(:move_kie_ber) { FactoryGirl.create(:move_order, unit: army_kie, destination: prov_ber) }
    end

    context "Diagram5", diagram: 5 do
      subject { resolved_orders }
      example { expect(subject.find(move_ber_pru).status).to eq OrderStatus::FAILURE }
      example { expect(subject.find(move_kie_ber).status).to eq OrderStatus::FAILURE }
    end
  end
end

MainPhase#resolve_pileup メソッド

メソッド名が適当になってきた。

MainPhase#resolve_pileup の仕様

  • 移動先が塞がってたら移動失敗。
  • 移動先に未解決の移動命令があったら処理保留。
  • 玉突き衝突解決のためにループ処理。

まず MoveOrder#hold? に手を入れる。

class MoveOrder < Order
  #(略)

  def hold?
    # 失敗した移動命令は維持と同様に扱う
    return true if failure?
    return true if standoff?
    false
  end

  #(略)
end

MainPhase#resolve_move もちょっと修正。

MainPhase#resolve_move の仕様

  • 移動先に維持命令を受けた軍があったら処理保留。
  • 移動先が競合する移動命令があったら処理保留。
  • 移動先に未解決の移動命令があったら処理保留。 ←New!
  • 残りは移動成功。

現時点では未処理命令に交換移動セット*1や循環移動セット*2があると、resolve_pileup で無限ループに突入してしまうので注意。

交換移動は Diagram6、循環移動は Diagram7 で実装する。

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

  #(略)

  # 障害のない移動
  def resolve_move
    moves = @orders.select{|o| o.move? && o.unexecuted?}
    moves.collect{|m| m.dst}.uniq.each do |dst|
      hold = @orders.select{|o| o.hold? && o.province == dst}[0]
      next if hold

      move = @orders.select{|o| o.move? && o.src == dst}[0]
      next if move

      move, *conflicts = moves.select{|m| m.dst == dst}
      next if conflicts.size != 0

      move.success!
    end
  end

  #(略)

  # 玉突き衝突
  def resolve_pileup 
    loop do
      moves = @orders.select{|o| o.move?}
      break if moves.size == 0

      moves.collect{|m| m.dst}.uniq.each do |dst|
        hold = @orders.select{|o| o.hold? && o.province == dst}[0]
        next unless hold

        moves.select{|m| m.dst == dst}.each do |m1|
          m1.failure!
        end
      end
    end
  end

  #(略)
end

ついでに Order#failure! も実装。

class Order <  ActiveRecord::Base
  #(略)

  def failure!
    self.status = OrderStatus::FAILURE
  end

  #(略)
end

メソッド名は Order#fail! の方が良さげかもしれない。後で考える。

*1:A→B、B→A みたいなやつ。

*2:A→B、B→C、C→A みたいなやつ。

gem: Spring

Rails Application Preloader

そもそもプリローダとは何ぞやと問えば、Rails アプリを事前にロードしておくことで rails rake コマンドを高速化するためのものだ。Spring の他には Zeus や Spork がある。

Spork は少しだけ使っていたことがあるが、事前の設定やら再起動が面倒で個人的にはあまり使い勝手がよろしくなかった。Zeus は知らない。

Spring は gem をインストールするだけで設定などは不要とのこと。

ただし、git のブランチを移動した場合などは再起動のため一度停止しなければならない。まあそれは当たり前か。

$ spring stop

Gemfile に書きたかったら書けばいいじゃない

Install the spring gem. You can add it to your Gemfile if you like but it's optional. You now have a spring command. Don't use it with bundle exec or it will be extremely slow.

Readme - Usage

要は「bundle exec と一緒に使うとめっちゃ遅くなるからやめとけ」とのことらしいので、素直に gem install でインストール。rbenv 環境なので rbenv rehash も忘れずに。

試してみた

まず一度停止。

$ spring stop
Spring stopped.

実行。

$ time spring rake spec
(略)
spring rake spec  0.11s user 0.02s system 2% cpu 4.554 total

もういっちょ。

$ time spring rake spec
(略)
spring rake spec  0.11s user 0.03s system 5% cpu 2.775 total

確かに速くはなっているのだが、まだクラスもテストケースも全然少ないせいか期待していたほどではないな……。

他にも試してみた

2回目以降の rails c が一瞬で起動する。すげえ。Spring 最高。

MOE3: スタンドオフの解決

spec/models/main_phase_spec.rb

まずテストを書く。

マニュアルの Diagram4、German A Ber–SilRussian A War–Sil のスタンドオフ処理を実装する。

f:id:asagix:20130910224007p:plain:w200

ちなみに Diagram2 は 1 と同じ単純移動(ただし海軍)なので省略。

f:id:asagix:20130910224005p:plain:w200

Diagram3 は海軍にとっての非隣接地域への移動不可に関する事例ということで、MainPhase#resolve_orders の守備範囲外(不可能命令は登録する段階で弾く予定)なので省略。

f:id:asagix:20130910224006p:plain:w200

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

    #(略)

    shared_context "Diagram4 スタンドオフ", diagram: 4 do
      let(:army_ber) { FactoryGirl.create(:army, :g, :ber, phase: phase) }
      let(:army_war) { FactoryGirl.create(:army, :r, :war, phase: phase) }
      let(:prov_sil) { Province.find_by(code: "sil") }

      let!(:move_ber_sil) { FactoryGirl.create(:move_order, unit: army_ber, destination: prov_sil) }
      let!(:move_war_sil) { FactoryGirl.create(:move_order, unit: army_war, destination: prov_sil) }
    end

    context "Diagram4", diagram: 4 do
      subject { resolved_orders }
      example { expect(subject.find(move_ber_sil).status).to eq OrderStatus::STANDOFF }
      example { expect(subject.find(move_war_sil).status).to eq OrderStatus::STANDOFF }
    end
  end
end

MainPhase#resolve_standoff メソッド

ここでも戦力評価の実装は後回し。

  • 未処理の MoveOrder オブジェクトを全て取得。
  • 移動先一つ一つについて競合する移動命令があるかチェック。
  • 移動先が競合した場合は問答無用でスタンドオフにする。

なんやかんやでこの形。

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

  #(略)

  # スタンドオフ
  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 |m1|
        m1.standoff!
      end
    end
  end

  #(略)
end

MoveOrder#move? は成否を問わず処理済みなら false を返すようにしてみた。

class MoveOrder < Order
  #(略)

  def move?
    # 処理済の移動命令は移動命令として扱わない
    return true if unexecuted?
    false
  end

  def standoff!
    self.status = OrderStatus::STANDOFF
  end
end