kairo-gokko (25) 状態変更の伝播の過程を見たい



前回は状態更新処理をループさせることで 2段、3段とリレーが連なった場合でも動くようにしました。

前回の回路はうまく動きましたが、そういえば無限ループになる場合はないのでしょうか?

あります。 たとえば次の回路では無限ループが発生します。

f:id:sonota88:20200316053355p:plain

困ったことに初回の状態更新で無限ループが発生し、 いつまでたっても画面に何も描画されません。

うーむ、困りましたね…… とりあえずは無限ループが発生したら異常終了させときましょうか。 つまり、こういう状態更新が収束しないようなウロボロス的回路は不正とみなすことにするわけです。

--- a/main.rb
+++ b/main.rb
@@ -57,8 +57,11 @@ end
 
 def update_tuden_relay_switch_lamp(circuit)
   switch_changed = true
+  count = 0
 
   while switch_changed
+    count += 1
+    raise "Too many state updates" if 1000 < count
+
     circuit.update_tuden_state()
     switch_changed = circuit.update_not_relays_state()
     circuit.update_lamps_state()

とりあえずはこれでいいかな……と思ってしばらくこれで進めていましたが、 次に挙げる 2つの理由により、無限ループになるような回路でも不正とみなさず動かし続けることにしました。


理由の1つめは、ループが止まらない回路があとで実際に登場してくるためです。

今作っているのは 2周目で、あとで必要になることがすでに分かっているので、 もうやってしまってもいいかなと。

しばらくは「そういう回路を描かなければいい」で回避してもいいんですけどね (プロトタイプのときは実際そうしてました)。 上に上げた回路の例も無限ループになる例として挙げただけであって、今そういう回路を動かしたいわけではありませんし。

なので、1つ目は今の時点ではちょっと弱い理由です。


もう一つの理由の説明のために、子回路が4つある前回の回路を動かした GIF 画像を再掲します。

f:id:sonota88:20200315143609g:plain

これ、うまく動いて万々歳でしたね。すばらしい。

すばらしくはありますが、何かこう、物足りなさが……。

何が物足りないか?

一番左のスイッチをマウスでクリックして切り替えると、 4つの子回路の状態が瞬時に切り替わります。

瞬時に切り替わるのはなぜか。 それは、一番左のスイッチを切り替えると、 状態変更が収束するまでループして、 状態変更が止まったらようやくそこで結果だけをポンと描画しているからですね。

リレーによって順番に左から右へと影響が波及しているはずなのに、 最終的な結果だけが一瞬で表示される作りになっているため、 その過程が見えず、つまらないなと。 あと、せっかくがんばって(?)処理してるのにそれが見えないのはちょっともったいないような。

また、「理屈ではそうなると分かっていて、実際に動かした結果も予想に合致しているけど、実感が伴わない」ような感じもあります。

ドミノのように、パタパタと順番に切り替わっていく様子が目に見えると、 ピタゴラ装置感が出ておもしろくなるんじゃないでしょうか!?

……というのが 2つ目の理由です。 どっちかというとこっちの理由の方を優先します。


というわけで、「結果だけポンと描画」方式をやめて、 影響が波及していく過程が見えるように変更します。 これにより、状態変更が収束しない回路も動かし続けられるようになります。


まずはさっきの修正(無限ループが発生したら異常終了するようにする)を取り消し。


それから

  • (1) while ループをなくし、update_tuden_relay_switch_lamp() 内では状態更新を繰り返さないようにする
  • (2) ローカル変数 switch_changed の代わりに Circuit#switch_changed に(リレーによる)スイッチの状態変化を記憶させる
    • メソッドのスコープを越えて記憶させておくため
  • (3) update_tuden_relay_switch_lamp() を呼び出す条件に Circuit#switch_changed を追加
    • 手動によるスイッチの変化、またはリレーによるスイッチの変化があったら 通電判定以下をやりなおす
--- a/circuit.rb
+++ b/circuit.rb
@@ -13,6 +13,7 @@ end
 
 class Circuit
   attr_reader :child_circuits
+  attr_accessor :switch_changed
 
   def initialize(child_circuits)
     @child_circuits = child_circuits


--- a/main.rb
+++ b/main.rb
@@ -56,13 +56,9 @@ def on_push_switch(pushed_switch)
 end
 
 def update_tuden_relay_switch_lamp(circuit)
-  switch_changed = true
-
-  while switch_changed
     circuit.update_tuden_state()
-    switch_changed = circuit.update_not_relays_state()
+    circuit.switch_changed = circuit.update_not_relays_state()
     circuit.update_lamps_state()
-  end
 end
 
 def draw(view, circuit, mx, my)
@@ -130,7 +126,7 @@ def main_loop(circuit, view)
     end
   end
 
-  if switch_changed
+  if switch_changed || circuit.switch_changed
     update_tuden_relay_switch_lamp(circuit)
   end

さらっとした diff ですが、ここらへんもプロトタイプの時はいろいろあったんですよ…… (割愛)。

これで途中のステップごとに描画されるようになったはずなんですが、実際動かしてみると速すぎて一瞬で切り替わってるように見えますね……。

そこで、一定の時間が過ぎるまで状態更新をスキップするような制御を追加します。

--- a/circuit.rb
+++ b/circuit.rb
@@ -14,9 +14,11 @@ end
 class Circuit
   attr_reader :child_circuits
   attr_accessor :switch_changed
+  attr_accessor :last_update
 
   def initialize(child_circuits)
     @child_circuits = child_circuits
+    @last_update = Time.at(0)
   end
 
   def to_plain


--- a/main.rb
+++ b/main.rb
@@ -56,6 +56,9 @@ def on_push_switch(pushed_switch)
 end
 
 def update_tuden_relay_switch_lamp(circuit)
+  return if Time.now < circuit.last_update + 0.2
+  circuit.last_update = Time.now
+
   circuit.update_tuden_state()
   circuit.switch_changed = circuit.update_not_relays_state()
   circuit.update_lamps_state()

こうなりました!

f:id:sonota88:20200317053554g:plain

うーん、良い。断然こっちの方が楽しいでしょ。

f:id:sonota88:20200317054754g:plain

状態更新が収束しない回路も動かせるようになりました!


あ、そうだ、状態更新のタイミングがずれるようになったので、 リレーの状態が変化したときも音を出すようにしましょう。

--- a/main.rb
+++ b/main.rb
@@ -62,6 +62,8 @@ def update_tuden_relay_switch_lamp(circuit)
   circuit.update_tuden_state()
   circuit.switch_changed = circuit.update_not_relays_state()
   circuit.update_lamps_state()
+
+  Sound[:relay].play if circuit.switch_changed
 end
 
 def draw(view, circuit, mx, my)
@@ -140,14 +142,15 @@ end
 
 circuit = Circuit.from_plain(parse_json($data_json))
 
-update_tuden_relay_switch_lamp(circuit)
 
 view = View.new(PPC)
 
 Sound.register(:click, "click.wav")
+Sound.register(:relay, "relay.wav")
 
 Window.load_resources do
   hide_loading()
+  update_tuden_relay_switch_lamp(circuit)
 
   Window.bgcolor = C_BLACK
 

--- a/run.sh
+++ b/run.sh
@@ -11,6 +11,9 @@ fi
 bundle exec ruby gen_sound.rb \
   out=click.wav amp=0.05 msec=30 hz=1000
 
+bundle exec ruby gen_sound.rb \
+  out=relay.wav amp=0.05 msec=30 hz=500
+
 ruby preprocess.rb "$@" > data.rb
 
 if [ "$BROWSER" = "1" ]; then

以下の iframe で実際に動かせます。 音量小さめにしていますが音が出ます。

こちらも同じものです。
https://sonota88.github.io/kairo-gokko/pages/25/index.html