MOE3: SupportOrder のテスト
Order#target と Order#target=
理想は TDD なんだけど、実際に処理を書いてみないと挙動のイメージがつかめないことが多いので、どうしてもある程度コードが形になってからテストを用意するスタイルになってしまう。
安定してきたらテスト先行に移れると思うけど当面はやむなしか。
実装は Order
だけど、実際に使用されるのは SupportOrder
か ConvoyOrder
なので、SupportOrder
のテストとして書いた。ConvoyOrder
のテストは書くとしても差分が必要な場合だけになる予定。
spec/models/support_order_spec.rb
初めて shared_context
と let
を活用してみたので、いろいろ実験した結果長くなった。試しに全文貼ってみる。
正直 SupportOrder#new
で生成した場合の挙動や target
属性未指定で生成した場合の挙動はテストする必要があったのか疑問だけど、「どうなるか分からない」で放置するのも怖かったので書くだけ書いておいた。
行軍解決処理のテストはこの何倍も長くなるんだろうなあ。
# -*- coding: utf-8 -*- require 'spec_helper' describe SupportOrder do let(:hold_order) { FactoryGirl.create :hold_order } let(:move_order) { FactoryGirl.create :move_order } shared_context "SupportOrder#new で生成", support_order_gen: :new do let(:support_order) { FactoryGirl.build :support_order, target: target } end shared_context "SupportOrder#create で生成", support_order_gen: :create do let(:support_order) { FactoryGirl.create :support_order, target: target } end shared_context "target が nil の場合", target: :nil do let(:target) { nil } end shared_context "target が HoldOrder の場合", target: :hold do let(:target) { hold_order } end shared_context "target が MoveOrder の場合", target: :move do let(:target) { move_order } end describe "#new", support_order_gen: :new do context "target が nil の場合", target: :nil do subject { support_order.target } example { expect(subject).to be_nil } end context "target が HoldOrder の場合", target: :hold do subject { support_order.target } example { expect(subject).not_to be_nil } end context "target が MoveOrder の場合", target: :move do subject { support_order.target } example { expect(subject).not_to be_nil } end end describe "#create", support_order_gen: :create do context "target が nil の場合", target: :nil do subject { support_order.target } example { expect(subject).to be_nil } end context "target が HoldOrder の場合", target: :hold do subject { support_order.target } example { expect(subject).not_to be_nil } end context "target が MoveOrder の場合", target: :move do subject { support_order.target } example { expect(subject).not_to be_nil } end end describe "#target", support_order_gen: :new do context "target が HoldOrder の場合", target: :hold do subject { support_order.target } example { expect(subject).not_to eq hold_order } example { expect(subject.id).to be_nil } example { expect(subject.sample).to be_true } example { expect(subject.unit).to eq hold_order.unit } example { expect(subject.province).to eq hold_order.province } example { expect(subject.persisted?).to be_false } end context "target が MoveOrder の場合", target: :move do subject { support_order.target } example { expect(subject).not_to eq move_order } example { expect(subject.id).to be_nil } example { expect(subject.sample).to be_true } example { expect(subject.unit).to eq move_order.unit } example { expect(subject.src).to eq move_order.src } example { expect(subject.dst).to eq move_order.dst } example { expect(subject.persisted?).to be_false } end end describe "#target", support_order_gen: :create do context "target が HoldOrder の場合", target: :hold do subject { support_order.target } example { expect(subject).not_to eq hold_order } example { expect(subject.id).not_to be_nil } example { expect(subject.sample).to be_true } example { expect(subject.unit).to eq hold_order.unit } example { expect(subject.province).to eq hold_order.province } example { expect(subject.persisted?).to be_true } end context "target が MoveOrder の場合", target: :move do subject { support_order.target } example { expect(subject).not_to eq move_order } example { expect(subject.id).not_to be_nil } example { expect(subject.sample).to be_true } example { expect(subject.unit).to eq move_order.unit } example { expect(subject.src).to eq move_order.src } example { expect(subject.dst).to eq move_order.dst } example { expect(subject.persisted?).to be_true } end end describe "#target=", support_order_gen: :new do context "後から設定", target: :nil do before do support_order.target = hold_order end subject { support_order.target } example { expect(subject).not_to be_nil } example { expect(subject).not_to eq hold_order } example { expect(subject.unit).to eq hold_order.unit } example { expect(subject.province).to eq hold_order.province } example { expect(subject.persisted?).to be_false } end context "書き換え", target: :hold do before do @lt = support_order.target support_order.target = move_order end subject { support_order.target } example { expect(subject).not_to be_nil } example { expect(subject).not_to eq move_order } example { expect(subject.unit).to eq move_order.unit } example { expect(subject.src).to eq move_order.src } example { expect(subject.dst).to eq move_order.dst } example { expect(subject.persisted?).to be_false } example { expect(@lt.persisted?).to be_false } end end describe "#target=", support_order_gen: :create do context "後から設定", target: :nil do before do support_order.target = hold_order end subject { support_order.target } example { expect(subject).not_to be_nil } example { expect(subject).not_to eq hold_order } example { expect(subject.unit).to eq hold_order.unit } example { expect(subject.province).to eq hold_order.province } example { expect(subject.persisted?).to be_true } end context "書き換え", target: :hold do before do @lt = support_order.target support_order.target = move_order end subject { support_order.target } example { expect(subject).not_to be_nil } example { expect(subject).not_to eq move_order } example { expect(subject.unit).to eq move_order.unit } example { expect(subject.src).to eq move_order.src } example { expect(subject.dst).to eq move_order.dst } example { expect(subject.persisted?).to be_true } example { expect(@lt.persisted?).to be_false } end end end
Order#target= リファクタリング
少しだけ綺麗になった。
class Order < ActiveRecord::Base #(略) # target に渡された Order は自動的に複製される alias_method :association_target=, :target= def target=(target) if target sample = target.dup sample.sample = true else sample = nil end self.target.destroy if self.target self.association_target = sample save! if persisted? end #(略) end
belongs_to
宣言によって設定された Order#target=
メソッドは Order.create
(および Order.new
)の実行時にも呼ばれる。
引数として nil
と Order
の派生クラスのオブジェクト以外が渡された場合の動きは考慮していないが、その場合は sample.sample = true
で NoMethodError
例外が飛ぶ。
万が一 sample=
メソッドを持つオブジェクトでも self.assosiation_target = sample
で ActiveRecord::AssociationTypeMismatch
例外になるので検出漏れはない。
例外が飛ぶ時点で呼び出し側のバグは明白なので、Order#target=
としてはこれ以上の配慮は不要だろう。
MOE3: Order モデル実装
スキーマ
viaconvoy
と sample
のデフォルト値は false
。
create_table "orders", force: true do |t| t.integer "player_id" t.integer "unit_id" t.integer "destination_id" t.boolean "viaconvoy", default: false t.integer "target_id" t.boolean "sample", default: false t.string "type" t.datetime "created_at" t.datetime "updated_at" end
Order クラス
デリゲーター Order#province
を定義。
tareget
関連の小細工は勢いだけで書いたので、ちゃんとテストしないと心配。
もうちょっと綺麗に書けないものか。
class Order < ActiveRecord::Base extend Forwardable belongs_to :player belongs_to :unit belongs_to :destination, class_name: "Province" belongs_to :target, class_name: "Order", dependent: :destroy # Order#province を定義 def_delegator :unit, :province # target に渡された Order は自動的に複製される alias_method :association_target=, :target= def target=(target) return unless target self.target.destroy if self.target sample = target.dup sample.sample = true self.association_target = sample save! if persisted? end end
もちろん target
も target=
も SupportOrder
と ConvoyOrder
でしか使わない。後々問題になるようなら HoldOrder
と MoveOrder
のは塞ぐけど、実害がなければこのまま放置。
ところで、Order#target=
の戻り値を複製された sample: true
のオブジェクトにしたかったのだが、どうも Ruby の Setter メソッドの戻り値は変更できないようだ。特に必要はないので別に構わないんだけど。
HoldOrder クラス
今は書くことなし。
class HoldOrder < Order end
MoveOrder クラス
MoveOrder#src
と MoveOrder#dst
も定義。
一見すると似たようなメソッドなのに、どちらも通常のメソッド定義じゃないところが面白い。
class MoveOrder < Order # MoveOrder#src を定義 def_delegator :unit, :province, :src # MoveOrder#dst を定義 alias_method :dst, :destination end
SupportOrder クラスと ConvoyOrder クラス
今は書くことなし。
class SupportOrder < Order end
class ConvoyOrder < Order end
MOE3: 輸送命令
MOE2 の輸送命令
MOE2 で輸送命令を出す場合、輸送対象の陸軍が明示的に海路移動を宣言している必要がある。
輸送命令を指定する際の操作と行軍解決処理の簡易化と「おせっかい輸送」回避のためだが、実はこの仕様だと本来のルール上は可能なはずの「自動海路選択」が適用されない。
おせっかい輸送
例えば、Hol に独陸軍、Bel に仏海軍がいるとしよう。
独は「Bel の仏海軍が Pic に後退するなら」Hol の陸軍を Bel に進めたいと考えて A Hol-Bel
を指定し、仏も同じ考えで F Bel-Hol
を指定していたとする。お互いに「進軍できれば儲けもの」程度の思惑で、実際には 1 vs 1 で状況は動かないはずだった。
ところが、何のつもりか、ここで英が Nth の海軍に対して、誰が頼んだわけでもない F Nth C German A Hol-Bel
を指示していた場合、仏独両軍にとっては想定外の入れ替えが発生してしまうことになる。
……本当に?
もちろん、それはルールで封じられている。
輸送についてのレアケース
陸軍が移動する際に陸路と海路両方の解釈が可能な場合、下記の通り処理する。
- 輸送海軍のどれか一つでも陸軍と所属国が同じなら海路。
- 輸送海軍の全てが対象の陸軍と所属国が異なるなら陸路。
- 陸軍が海路を宣言していれば海路。
というわけで「おせっかい輸送」は 2 で阻止される。
自動海路選択
むしろ「自動海路選択」、上記 1 のケースだが、MOE で輸送を利用する場合にはシステム上 3 を強制されるのでこのルールの出番がない。
正確には、海路宣言の強制によってレアケースルールの実装をサボったわけだ。
今のところ MOE の輸送仕様について特に問題視する声は聞こえてこないが、本来はルール上可能ならばシステム上も可能であるべきとは思う。
MOE3 での仕様
陸軍が海路利用を宣言していない場合でも、同じ所属国の海軍による輸送が成立していれば海路を選択したものと見なす。
どう実装すれば良いのか見通しは立っていないが、なるようになるだろう。
MoveOrder(移動命令)の海路宣言
というわけで、すっかり忘れてた。
MoveOrder モデルで必要な属性
- destination_id: 「どこへ移動するか」を示す Province への参照。
- viaconvoy: 海路利用を宣言する場合は
true
。 ← New!
Order モデルの属性
改めて。
Order モデルの属性
- player_id: 「誰が指示した命令か」を特定するための参照。
- unit_id: 「どの卓の何ターン目のどの軍への命令か」を特定するための参照。
- destination_id: 「どこへ移動するか」を示す Province への参照。
- viaconvoy: 海路利用を宣言する場合は
true
。- target_id: 「どの命令を」支援あるいは輸送するかを示す Order への参照。
- sample: 支援または輸送命令の
target
に設定される複製命令ならtrue
。- type: 単一テーブル継承用の属性。
今度こそ Order および派生モデルの実装だ。
MOE3: 仮想命令
従来の仮想命令
ディプロマシーでは他の軍を支援または輸送することができる。これは対象となる軍の所属国を問わない。
自国の軍を支援または輸送する場合、MOE ではまず対象となる軍の命令を登録し、支援または輸送命令登録の際に、先に登録してある命令の中から対象として選択可能な命令を指定する段取りになっている。
他国の軍については、行軍が解決されて公表されるまでその国が実際に出した命令を知ることはできないため、支援や輸送の対象として「この国のこの軍はこう動くはず」という自分専用の決め打ちの命令を登録できるようになっている。
その決め打ち命令を仮想命令と呼ぶ。
あくまで UI の都合で導入された概念のため、仮想命令は実際の行軍解決処理では全て無視され、命令履歴にも残らない。
もうひとつの仮想命令
上記仮想命令とは別に、SupportOrder および ConvoyOrder の「対象として参照される命令」も通常命令とは区別する必要がある。
例えば、支援命令 F Nrg S A Yor-Nwy
は SupporOrder F Nrg S
の target
属性に MoveOrder A Yor-Nwy
への参照が設定されたものだが、この時 A Yor-Nwy
は通常命令や仮想命令への直接の参照であってはならない。
通常命令も仮想命令も、支援あるいは輸送命令が設定された後で変更される可能性があるからだ。
命令の変更は「変更後命令の新規登録」と「変更前命令の削除」によって実現されるため、target
に直接の参照を設定すると、その命令が変更された場合に支援あるいは輸送命令の構成要素が欠落する不具合が生じ得る。
標本命令
その解決策として、SupportOrder#target
あるいは ConvoyOrder#target
を設定する際には、対象として指定した通常命令または仮想命令の複製オブジェクトを設定する。
単純に複製しただけでは複製元の命令と区別がつかないので、Order モデルの属性に boolean 型の sample
属性を追加する。
SupportOrder モデルと ConvoyOrder モデルで必要な属性
- target_id: 「どの命令を」支援あるいは輸送するかを示す Order への参照。
- sample: 支援または輸送命令の
target
に設定される複製命令ならtrue
。←New!
今後 Order#sample == true
のものを内部的に標本命令と呼び、通常命令および仮想命令と区別する。
Order モデルの属性
あらかた条件は出揃った。
Order モデルの属性
- player_id: 「誰が指示した命令か」を特定するための参照。
- unit_id: 「どの卓の何ターン目のどの軍への命令か」を特定するための参照。
- destination_id: 「どこへ移動するか」を示す Province への参照。
- target_id: 「どの命令を」支援あるいは輸送するかを示す Order への参照。
- sample: 支援または輸送命令の
target
に設定される複製命令ならtrue
。- type: 単一テーブル継承用の属性。
これでようやく Order および派生モデルの実装に入れる。
MOE3: Order モデル検討
Order と派生モデル
単一テーブル継承を使用するので、Order モデルだけではなく派生クラスで必要になる属性も先に考えておかなければならない。
必要になってから属性を追加するのがアジャイル流かもしれないが、明らかに必要になると分かっているものを先送りにするのはストレスなので。
ひとつひとつ見ていこう。
Order(命令)モデル
全ての命令の基底クラスとなり、Order インスタンスが生成されることはない。
Order モデルの属性
- player_id: 「誰が指示した命令か」を特定するための参照。
- unit_id: 「どの卓の何ターン目のどの軍への命令か」を特定するための参照。
- type: 単一テーブル継承用の属性。
HoldOrder(維持命令)モデル
何もしない命令。必要な属性は Order と同じはず。
だったら Order は捨てて HoldOrder を基底クラスにすればいいじゃん、という考えもないではないが、HoldOrder を特別扱いする合理的な理由がないのでこれで良い。
MoveOrder(移動命令)モデル
攻撃命令でもある。移動を伴うので「どこからどこへ」という情報が必要になる。
「どこから」は命令対象の軍が持っているが、「どこへ」は MoveOrder の固有の属性として持つ必要がある。
MoveOrder モデルで必要な属性
- destination_id: 「どこへ移動するか」を示す Province への参照。
「どこから」については MoveOrder#source
メソッドを書こう。
しかし行軍解決処理で多用するであろうことを考えると、MoveOrder#source
はともかく MoveOrder#destination
は好みで言えばメソッド名としてちょっと長い。
いっそのこと MoveOrder#src
MoveOrder#dst
にしてしまうか。要検討。
SupportOrder(支援命令)モデル
支援対象についての情報を持つ必要がある。
実際には軍だけではなく、その軍が受ける命令まで含めて指定する必要があるので、支援命令の対象となるのは軍そのものではなく、その軍が受けた命令となる。
SupportOrder モデルで必要な属性
- target_id: 「どの命令を支援するか」を示す Order への参照。
支援の成立と不成立
支援命令の対象となるのは、維持命令か移動命令のみ。
他国軍の維持を支援する場合、実際の支援対象が維持命令、支援命令、輸送命令のいずれであっても支援は成立するが、移動を支援するのであればその移動先が一致しなければならない。
- 維持・支援・輸送命令に対する維持支援はすべて成立する。
- 維持・支援・輸送命令に対する移動支援は成立しない。
- 移動命令に対する維持支援は成立しない。
- 移動先の異なる移動支援は成立しない。
- 結果的に失敗した移動命令に対する維持支援は成立しない。
ConvoyOrder(輸送命令)モデル
輸送対象についての情報を持つ必要がある。
こちらも正確を期すのであれば「どの軍の移動命令を輸送するか」と表現するのが正しい。日本語的には微妙だが気にしない。
target
属性については SupportOrder と共有できるだろう。
輸送の成立と不成立
輸送命令の対象となるのは移動命令、厳密には Coast から Coast への陸軍の移動命令のみ。
移動支援と同様に、移動先が一致しなければ輸送は成立しない。
- 輸送対象に移動命令以外を指定することはできない。
- 輸送対象となる移動命令は陸軍の Coast から Coast への移動のみ。
- 移動先の異なる輸送は成立しない。
Order モデルの属性
いったんまとめ。
Order モデルの属性
- player_id: 「誰が指示した命令か」を特定するための参照。
- unit_id: 「どの卓の何ターン目のどの軍への命令か」を特定するための参照。
- destination_id: 「どこへ移動するか」を示す Province への参照。
- target_id: 「どの命令を」支援あるいは輸送するかを示す Order への参照。
- type: 単一テーブル継承用の属性。
実は、これではまだ足りない。
仮想命令についてもう少し練り込む必要がある。
その他の命令
撤退フェイズの撤退命令と解隊命令、調整フェイズの増設命令と解隊命令を Order の派生クラスにするかどうかは検討中。
MOE3: Unit#to_s メソッド
Unit の文字列化
Unit#to_s
への要求仕様は至極単純。
仕様 1
例えば「Lon にいる海軍」の
#to_s
を呼んだら"F Lon"
を返す。陸軍なら
"A Lon"
を返す。
最初の文字が "A"
か "F"
かの違いだけなので、メソッド本体は Unit#to_s
として実装してしまい、差分を Army
と Fleet
で吸収する形が自然な発想だろう。
それからもう一点、この先 Order#to_s
に関係してくるはずの仕様として、引数に Power
を一つ渡せるようにする。
仕様 2
引数として
Power
が渡された場合、その軍の所属国、すなわちUnit#power
と引数のPower
が一致しなければ、Unit#to_s
は戻り値に所属国のgenitive
を付与する。
前述の「Lon にいる海軍」の所属国が英で、Fleet#to_s
に渡された Power
が仏であれば、Fleet#to_s
は "English F Lon"
を返すのが正しい。
実装
実際に呼ばれるのは Army#to_s
または Fleet#to_s
であって、Unit#to_s
が直接呼ばれることはない。しかし派生クラスにしか存在しない symbol
メソッドを Unit#to_s
から呼び出すのが気持ち悪かったのでこうなった。
class Unit < ActiveRecord::Base belongs_to :phase belongs_to :power belongs_to :province def symbol # 派生クラスでオーバーライド end def to_s(owner = power) result = "%s %s"%[symbol, province.shortname] result = "%s %s"%[power.genitive, result] if owner != power result end end
徹底するなら Unit#symbol
では例外投げるべきだろうけど、単なる気休めなのでそこまでは必要なかろう。
class Army < Unit def symbol "A" end end
class Fleet < Unit def symbol "F" end end
ちなみに Province#shortname
はメソッド、Province#code
と Power#genitive
は属性である。
class Province < ActiveRecord::Base belongs_to :homepower, class_name:"Power" def shortname code.capitalize end end
class Power < ActiveRecord::Base end
MOE3: Unit モデル実装
Unit(軍)モデル
Order モデルの実装の前に Unit を作ってしまおう。
Army(陸軍)と Fleet(海軍)は例によって単一テーブル継承。
Phase はまだイメージが固まってないので後回し。
スキーマ
シンプル。
create_table "units", force: true do |t| t.integer "phase_id" t.integer "power_id" t.integer "province_id" t.string "type" t.datetime "created_at" t.datetime "updated_at" end
クラス定義
シンプル。
class Unit < ActiveRecord::Base belongs_to :phase belongs_to :power belongs_to :province end
class Army < Unit end
class Fleet < Unit end