MOE3: 支援付き攻撃の解決
spec/models/main_phase_spec.rb
いよいよ本番といった感じ。
マニュアルの Diagram8、French A Mar–Bur
、French A Gas S A Mar-Bur
、German A Bur-Holds
の支援、攻撃、敗退処理を実装する。
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–Hol
、French A Hol–Bel
、French F Bel-Nth
の循環移動処理を実装する。
ここまでの実装だと、それぞれ移動先に未解決の移動命令があるため成否が確定しない。
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
に切り出して再帰呼び出し。
moves
と move1
は固定で、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–Pru
と German A Pru–Ber
の交換移動禁止処理を実装する。
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–Pru
と German A Kie–Ber
の移動失敗の連鎖処理を実装する。
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!
の方が良さげかもしれない。後で考える。
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 aspring
command. Don't use it withbundle exec
or it will be extremely slow.
要は「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–Sil
と Russian A War–Sil
のスタンドオフ処理を実装する。
ちなみに Diagram2 は 1 と同じ単純移動(ただし海軍)なので省略。
Diagram3 は海軍にとっての非隣接地域への移動不可に関する事例ということで、MainPhase#resolve_orders
の守備範囲外(不可能命令は登録する段階で弾く予定)なので省略。
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