SVX日記
2023-05-01(Mon) つまらないFactorio
「一番最初のサラ地から遊びたいが製品版を買いたくないなぁ」ということで、5番目の体験版シナリオの既設の施設を全部回収し、サラ地状態にしてから始めることにした。意外と大した手間ではなかった。
2023-05-02(Tue) さらにつまらないFactorio
製品版を買ってしまったところで、完全な初期状態から始める。久々の手作業での採掘がダルいが、最初から赤色の自動化サイエンスパック、緑色の物流サイエンスパックの量産を想定し、前回作ったラインを更に改善しつつ再構築。
2023-05-05(Fri) 割合生産するFactorio
Factorioを始めて、しばらくして気づいたのが「ベルトに複数のアイテムを混在させて運ぶと詰まってしまう」ということだ。それを防ぐには「ベルトに複数のアイテムを混在させない」ことだ。そうすれば「消費が遅く、生産能力が過剰」か「消費が早く、生産能力が不足」のどちらかにはなるものの、詰まってしまうことはなくなり、人手を介入する必要はなくなる。ここ数日はそれを目指して、赤色、緑色、灰色のサイエンスパックの量産ラインを完成させてきた。
ちょうど近くまで鉄鉱石を運ぶベルトが延びており、その片側が空いていたため、それを活用し、プラスチック棒と硫黄を混在して運ぶことにした。しかし、それをすると、どうしても詰まってしまうのだ。新たにベルトを敷けば済むのだが、どうにかする方法はないものなのかと。
両アイテムが常にチェストに補充される状態にして、何らかの方法で必要な割合で取り出せるようにしたら? と思ったが、チェストの容量は有限なので、生産を抑制しない限りどちらかが余り、いつかはチェストの容量の上限に達してしまう。つまり、必要な量しか、組立機や化学プラントから取り出さないようにしなければならないことになる。今回の目標は、プラスチック棒と硫黄を6:1で取り出し、供給することだ。
そうなるとインサータの制御は必須なので、回路を組むのが必須になる。ループするベルトの上に取り出したいアイテムを見本として置き、それをベルトに検知させて、インサータに取り出させてはどうか? と思ったが、これもダメ。ベルト上をアイテムが通過するのには一定時間を要するし「消費が遅く、生産能力が過剰」な場合、ベルト上にアイテムが滞留し、インサータの動作回数が制御できない。パルスで動作をトリガする必要がある。
やり方を思いついては、試してうまく動作せず、の繰り返し。数十時間を費やしても実現できない。回路も組めるプログラマとして、プライドはズタズタだ。どうやら、変数がなかったり、論理演算にクセがあったり、コンデンサのようなタイマがなかったりと、Factorioの回路を、既存の概念の範疇で捉えようとすることが誤りのように思えてきた。
そして気づいたのが「赤い*」のAND動作のクセだ。%演算子でループするカウンタを作ったまではいいのだが、それを「赤い*」に食わせると意図しない動作になってしまう。散々アレコレやっているうちにようやく気づいた。「ゼロは偽ではなく、無信号だ」ということ。別の言い方をすると「ゼロは、ANDに偽の信号を入力するのでなく、信号線そのものを外す動作となる」ということだ。
これに気づいたらだいぶ進捗した。ゼロを使わないようにすればいい。定数回路を使って、ゲタを履かせてやればいい。真偽はn>9で判定するようにし、ループするカウンタは10〜15で常に真、パルスは9と10を発生させて一瞬だけ真を発生させる。そのANDを取れば、10〜15の値がパルス出力される。それをインサータに入力し、動作条件をn>9とすれば6/6動作。n>14とすれば1/6動作となる。そいつを合流させれば6:1でアイテムを流せる。
パルスの発生にはループ状のベルトの上に載せた石炭と石を使う。アイテムの消費が遅くベルトの先が詰まった場合には、ベルトに検知させてループ状のベルトの回転を止め、パルスの発生自体を遅延させる。これでようやく期待する回路を組むことができた。そこそこシンプルな形で。
2023-05-10(Wed) 再帰で悩みまくる
何度か書いたことはあるのだが、ちょっと難しいのは確か。こうなったら、回り道ではあるが、よく知っている構造についてまず書いてみて、それをベースにすればいいのではないか。と思い、ファイル/ディレクトリ構造を表示する、プログラムを書いてみた。
#!/usr/bin/ruby
def find(path)
entry = {
:type => false,
:name => path.last,
}
unless(FileTest.directory?(path.join('/')))
entry[:type] = 'file'
else
entry[:type] = 'dir'
entry[:entries] = []
Dir.open(path.join('/')).each {|e|
e =~ /^\./ and next
entry[:entries] << find(path + [e]) # 再帰
}
end
return(entry)
end
def show(entry, n = 0)
puts('%s%4s: %s' % ["\t" * n, entry[:type], entry[:name]])
if(entry[:type] == 'dir')
entry[:entries].each {|entry1|
show(entry1, n + 1) # 再帰
}
end
end
entry = find(['fileA'])
show(entry)
puts
entry = find(['dirB'])
show(entry)
$ ./recursive
file: fileA
dir: dirB
file: fileC
dir: dirD
file: fileE
file: fileF
file: fileG
2023-05-12(Fri) ライン設計の計算機でFactorio
なんで再帰プログラムが書きたかったのかというと、なんのこたーない、原因はFactorioなのであった。
アイテムがつまらないラインを作れるようになると、次に浮上してくる問題がラインの稼働率が上がらないという現象だ。つまり、アイテムの生産速度を合わせないとボトルネックが発生してしまう。早速、手計算でやり始めたのだが、頭がコンガラがるし面倒くさすぎる。これは計算機にやらせるべき課題以外の何物でもない。
つうわけで、目的のアイテムを材料まで分解しながら、速度を合わせるためには「組立機が何台設置したらいいのか」を計算させる計算機を作ってみた。この「材料まで分解していく」処理で再帰の手法を用いると、プログラムがスッキリと書けるのである。先日書いたプログラムがベースだが、面白いくらいにほとんどそのまんまである。
#!/usr/bin/ruby
# Usage
# $ ./fa_calc 歯車
# $ ./fa_calc 赤パック '1.0/5'
@items = {
'鉄板' => {
:type => :mate,
},
'銅板' => {
:type => :mate,
},
'歯車' => {
:type => :prod,
:num => 1,
:time => 0.5,
:parts => [
{ :name => '鉄板', :num => 2, },
],
},
'赤パック' => {
:type => :prod,
:time => 5,
:parts => [
{ :name => '銅板', :num => 1, },
{ :name => '歯車', :num => 1, },
],
},
}
def disasm(part_name, num = 1)
prod = @items[part_name]
parts = {
:name => part_name,
:num => (num = num.to_f),
}
if(prod[:type] == :mate)
parts[:type] = :mate
else
parts[:type] = :prod
parts[:list] = []
prod_num = (prod[:num] || 1).to_f
parts[:macs] = prod[:time] * num / prod_num
prod[:parts].each {|part|
parts[:list] << disasm(part[:name], part[:num] * num / prod_num)
}
end
return(parts)
end
def show(parts, n = 0)
if(parts[:type] == :mate)
puts('%s%s: %s %5.2f' % ["\t" * n, parts[:type], parts[:name], parts[:num]])
else
puts('%s%s: %s %5.2f macs:%5.2f' % ["\t" * n, parts[:type], parts[:name], parts[:num], parts[:macs]])
parts[:list].each {|part|
show(part, n + 1)
}
end
end
part_name = ARGV[0]
num = eval(ARGV[1] || '1').to_f
puts("[%s x %5.2f]" % [part_name, num])
parts = disasm(part_name, num)
show(parts)
__END__
$ ./fa_calc 歯車
[歯車 x 1.00]
prod: 歯車 1.00 macs: 0.50
mate: 鉄板 2.00
$ ./fa_calc 銅線
[銅線 x 1.00]
prod: 銅線 1.00 macs: 0.25
mate: 銅板 0.50
$ ./fa_calc 歯車 2
[歯車 x 2.00]
prod: 歯車 2.00 macs: 1.00
mate: 鉄板 4.00
$ ./fa_calc 銅線 4
[銅線 x 4.00]
prod: 銅線 4.00 macs: 1.00
mate: 銅板 2.00
$ ./fa_calc 赤パック
[赤パック x 1.00]
prod: 赤パック 1.00 macs: 5.00
mate: 銅板 1.00
prod: 歯車 1.00 macs: 0.50
mate: 鉄板 2.00
$ ./fa_calc 赤パック '1.0/5'
[赤パック x 0.20]
prod: 赤パック 0.20 macs: 1.00
mate: 銅板 0.20
prod: 歯車 0.20 macs: 0.10
mate: 鉄板 0.40
$ ./fa_calc 緑基板
[緑基板 x 1.00]
prod: 緑基板 1.00 macs: 0.50
mate: 鉄板 1.00
prod: 銅線 3.00 macs: 0.75
mate: 銅板 1.50
$ ./fa_calc 緑基板 2
[緑基板 x 2.00]
prod: 緑基板 2.00 macs: 1.00
mate: 鉄板 2.00
prod: 銅線 6.00 macs: 1.50
mate: 銅板 3.00
さらに進む。緑基板を1個/秒で作るには、組立機0.5台、銅線の組立機0.75台が必要だが、材料として多用される緑基板はもっと早く作りたい。2個/秒で作りたいとなると、必要な銅線の組立機は1.5台となり、1台では足りず、銅線の作成がボトルネックになるという状況が明確に示される。
$ ./fa_calc 水パック '2.0/24'
[水パック x 0.08]
prod: 水パック 0.08 macs: 2.00
prod: エンジン 0.17 macs: 1.67
mate: 鋼材 0.17
prod: 歯車 0.17 macs: 0.08
mate: 鉄板 0.33
prod: パイプ 0.33 macs: 0.17
mate: 鉄板 0.33
prod: 赤基板 0.25 macs: 1.50
prod: 緑基板 0.50 macs: 0.25
mate: 鉄板 0.50
prod: 銅線 1.50 macs: 0.38
mate: 銅板 0.75
prod: プラ棒 0.50 macs: 0.25
mate: 石油ガス 5.00
mate: 石炭 0.25
prod: 銅線 1.00 macs: 0.25
mate: 銅板 0.50
prod: 硫黄 0.08 macs: 0.04
mate: 水 1.25
mate: 石油ガス 1.25
そして当面の課題だった「水パック」を計算してみる。着目すべきは、各項目の「macs」だ。エンジンで1.67、赤基板で1.5が出ている。自分が作ったラインでは、赤基板の作成がボトルネックになって水パックの作成速度が出ないのだが、エンジンは2台で作っているのに、赤基板を1台で作っている状況だ。まさに状況が明確に示されている。そうなんだよ、だから欲しかったんだよ、こんな計算機がさ。
増設はスペース的にキビしいので、線路をどうにかしないといかんが、赤基板の工作機は2台あれば十分ということも分かる。そうすると緑基板が不足しないか気になるが、赤基板1.5台に対しての必要数は0.25台と出ているので、現状の兼用でも足りるかな、と判断できる。
$ ./fa_calc 水パック | sed 's/^\s*//' | sort
[水パック x 1.00]
mate: 鋼材 2.00
mate: 水 15.00
mate: 石炭 3.00
mate: 石油ガス 15.00
mate: 石油ガス 60.00
mate: 鉄板 4.00
mate: 鉄板 4.00
mate: 鉄板 6.00
mate: 銅板 6.00
mate: 銅板 9.00
prod: エンジン 2.00 macs:20.00
prod: パイプ 4.00 macs: 2.00
prod: プラ棒 6.00 macs: 3.00
prod: 歯車 2.00 macs: 1.00
prod: 水パック 1.00 macs:24.00
prod: 赤基板 3.00 macs:18.00
prod: 銅線 12.00 macs: 3.00
prod: 銅線 18.00 macs: 4.50
prod: 硫黄 1.00 macs: 0.50
prod: 緑基板 6.00 macs: 3.00
ちなみに、ゲーム中で示されるトータルコストは、上記のようにすることで本計算機でも類似の値を算出できる。ゲーム中で示される水パックのトータルコストは「鉄板6、銅板15、プラ棒6、硫黄1、エンジン2」だ。銅板は6と9として別計上されているが合計すれば15で合っている。鉄板は合計が14と違っているが、これはエンジンに使われている8が入ってないせいで、それを抜けば6で合っている。要するに、どこまで分解するかの違いだ。本プログラムならルールの記述により、鉄板を鉄鉱石と石炭まで分解することもできる。
あまりに明確に結果が出てしまうので「ズルい」という感覚を受ける人も出てこようが、それはFactorioの本質をわかっていないと言えよう。こういう工夫をするところ、こういうプログラムを書くところからがFactorioなのだ。このプログラムを書いていた時間は、自分的にはFactorioで遊んでいた時間の一部なのである。
さらにいえば、如何に整然とした、手のかからないラインを組み上げるかがFactorioの楽しみだと思っているが、現状それには遠く及ばない状況である。しかし、整然としたラインを組み上げることが楽しみだと思うからこそ、再帰を使ってプログラムを組む必要があったのだ。再帰を使わずにプログラムを組むこともできようし、むしろその方が読みやすいかもしれないが、それは自分にとって整然としたコードではないのだ。
しかし、こんなに次々と新しい体験をさせてくれるゲームは始めてだな。ちょうどウランの採掘、精製を始めたところだが、使えるウランを取り出すのが如何に大変か、劣化ウランて、そういう意味だったのか、と気づかされたし、それより前には、フリップフロップを再習得させられたし、鉄道信号理論で閉塞区間についても学習させられたし……これから、原発の運営の難しさについても体験させてもらえるのだろうなぁ。
スクリプトを置いておく。
2023-05-20(Sat) コンテナのPVのバックアップを考える
しかしこれ、すごく普遍的な教訓をくれている気がする。というのも、そもそも一般にコンピュータシステムは、特定の作業を自動化するものであるが、自動化のレベルが低いほど、手作業でメンテする必要が生じてしまうものなのである。
よく、システムの作りがイマイチな場合に「運用でカバー」などという口当たりのいいコトバでの対策が取られるが、それはつまりシステム屋としての敗北なのである。言い訳無用で恥なのである。「こんなこともあろうかと」の対極なのである。
話をFactorioに戻すと、製造ラインを工夫している時間が作れない原因は、バイターの攻撃への対応の自動化のレベルが低いことであり、それはつまりFactorioプレイヤとしての敗北なのであり恥なのである。
そんな気づきを得た矢先、自分が職場で運用している自作のチャットシステムがトラブってしまった。結構、ガチで使われているシステムなので、めちゃくちゃ焦る。原因はディスクフルでGDBMが壊れたこと。GDBMを直そうとしたがダメ。しかし、直近の発言を表示するための履歴DBだったので、消しても影響なく単に消したら復旧できた。それでも原因の調査、究明と対策で1時間くらいを要してしまった。あまり長時間止まるとTeams以下の存在になってしまうから沽券に関わる。
そういうことなのだ。ちゃんと自動化しておけば、イザという時に焦らなくて済んだのだ。だから、これを機にバックアップ機構を作るべきだ……ということで作った。チャットシステム専用ではなく、コンテナのPVに対する汎用的な仕組みだ。リモートにrsyncするのが基本だが、ローカルにもできるようになっている。ちゃんと差分バックアップである。
そもそも、ロクにrsyncを使ったことがなかったし、シェルスクリプトも苦手意識があったが、今回の開発を通じ、その両方に少し詳しくなれた気がする。しかし、それにしても手間取ったなぁ。昔に比べて思考力が落ちているせいなのか、自らへの期待値が上がっているせいなのか、それ以外なのか……よくわからないが、こういう開発は楽しくて仕方ないものの、ここまで手間取るかなぁ、という印象を受けた。
diff --git a/docker-compose.yml b/docker-compose.yml
+# * Local Backup
+# BACKUP_PATH: pv/backup
+# BACKUP_EX: --exclude=backup
+# バックアップ先の領域として
+# mkdir pv/backup; chown 1000:1000 pv/backup しておくこと
+# * Remote Backup
+# BACKUP_HOST: user@rsync_ssh_host
+# BACKUP_PATH: backup
+# BACKUP_EX: --exclude=id_rsa*
+# バックアップ先への ssh アクセスのため
+# ssh-keygen -f pv/id_rsa; chown 1000:1000 pv/id_rsa しておくこと
+# CYCLE1: 3600
+# SCHED1: 300
diff --git a/Dockerfile b/Dockerfile
+# if [ -v BACKUP_PATH ]; then
+# if [ `date +%s` -ge $target1 ]; then
+# ((target1 += CYCLE1))
+# echo "`date`: Job easy cron 1 started."
+# stamp=`date +%Y%m%d_%H%M`
+# ssh_opt='-o StrictHostKeyChecking=no -o PasswordAuthentication=no'
+# if [ -v BACKUP_HOST ]; then
+# coron=:
+# ssh_opr="ssh $ssh_opt $BACKUP_HOST"
+# fi
+# last_backup=`$ssh_opr ls $BACKUP_PATH | tail -1`
+# backup_ex0='--exclude=hyperestraier'
+# if [ -z "$last_backup" ]; then
+# gen=0
+# else
+# gen=$((`$ssh_opr cat $BACKUP_PATH/$last_backup/.gen` + 1))
+# link_dest="--link-dest=../$last_backup"
+# fi
+# rsync -av --delete $backup_ex0 $BACKUP_EX -e "ssh $ssh_opt" pv $BACKUP_HOST$coron$BACKUP_PATH/$stamp $link_dest
+# echo $gen > .gen; scp $ssh_opt .gen $BACKUP_HOST$coron$BACKUP_PATH/$stamp
+# fi
+# fi
2023-05-23(Tue) 名状しがたいバックアップ方法を開発
最近のゲームでは少ないだろうが、例えば、売ってしまってはいけないアイテムを売ってしまい、その状態でセーブしたら、その後ゲームを進められなくなってしまう、などという状況がある。しかし、そういう場合、セーブエリア1,2, 3を順に使っていれば回避できる可能性がある。直前のセーブデータより、もうふたつ古い世代のセーブデータから始められるからだ。これは簡単に実行できる。
これを「何となく、ではなく」実現するのは難しい。例えば10章まで進んだ時点で5章のデータを残しておくことはできるが、20章まで進んだ時点では10章付近のデータが残っているべきなのだ。セーブエリアの数に制限がなければ考える必要はないが、6個とかに限定されるとやりくりする必要が生じる。
この問題は、ゲームのセーブデータに限らない。例えば、自分はヴォーカルの練習の記録を残してあるが、もう始めて5年くらいになるので、最近、記録容量が溢れそうになっている。後で成長の記録を確認したいと思えば、古いものや、2年半くらい前のものは残しておきたい。しかし、具体的にはどのように消していったらいいのか?
そしてPCのバックアップである。例えば、6回分の保持が可能な容量があるとして、直近の6世代を残すのはいい方法なのだろうか? それだと、1週間前にオペレーションミスしたことに気づいたら、あきらめるほかない。直前と、もうふたつ古い世代は残しておくとしても、残りの3回分は、もう少し他にやりようがあるのではないか?
動きを図化するとこんな感じだ。横軸がバックアップ世代、縦軸が時間の経過を意味している。概ねどの時間に着目しても、その時点の中間点と、中間点以降での中間点あたりの世代がまばらに残される動きになっている。フラクタル的にも見えるな。
backups = {}; last_gen = -1
Dir.glob('xxx_backups/*').each {|path|
gen = open('%s/.gen' % path).read.to_i
backups[gen] = path
gen > last_gen and last_gen = gen
}
ebackup = EspBackup.new
ebackup.stored = last_gen
backups.each {|gen, path|
ebackup.should_delete?(gen) and p('rm -rf %s' % path)
}
class EspBackup
def initialize(sgens = 3, lgens = sgens)
@sgens = sgens; @lgens = lgens
@bhs = [] # 3: [ '1110', '1100', '1000', '0100', '0110', '0111', '0000' ]
sgens.times {|i0|
i = sgens - i0 # sgens..1
@bhs << ('1' * i + '0' * (i0 + 1))
}
sgens.times {|i0|
i = i0 + 1 # 1..sgens
@bhs << ('0' + '1' * i + '0' * (sgens - i))
}
@bhs << '0' * (sgens + 1)
end
def stored=(gen)
@rgens = {}
@lgens.times {
@rgens[gen] = true
(gen -= 1) < 0 and return
}
bw = ('%b' % gen).size
@bhs.each {|bh|
(it = (bh + '0' * bw)[0, bw].to_i(2)) <= gen and @rgens[it] = true
@rgens.size == @sgens + @lgens and return
}
end
def rgens
@rgens.keys.sort
end
def should_delete?(gen)
!@rgens[gen]
end
end