SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|06|07|08|09|10|11|

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

  歯車を作るには、鉄板2枚が必要。銅線を作るには、銅板0.5枚が必要。ここで着目すべきは「macs」の値で、これが「組立機が何台設置したらいいのか」を示している。

$ ./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

  歯車を2個/秒で作るには、組立機1台が必要。銅線を4個/秒で作るには、組立機1台が必要。歯車は0.5秒で1つ、銅線は0.5秒で2つ、という生産時間が加味された結果となる。

$ ./fa_calc 赤パック
[赤パック x  1.00]
prod: 赤パック  1.00 macs: 5.00
	mate: 銅板  1.00
	prod: 歯車  1.00 macs: 0.50
		mate: 鉄板  2.00

  そんなのは手計算できるが、赤パックになると既にややこしくなってくる。赤パックを1個/秒で作るには、組立機5台が必要だが、歯車が1個/秒で必要になるので、組立機0.5台が必要、という結果になる。

$ ./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

  さすがに組立機5台で赤パックを1個/秒で作る必要はない、5秒に1個でいいよ、ということであればこうすればいい。

$ ./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の楽しみだと思っているが、現状それには遠く及ばない状況である。しかし、整然としたラインを組み上げることが楽しみだと思うからこそ、再帰を使ってプログラムを組む必要があったのだ。再帰を使わずにプログラムを組むこともできようし、むしろその方が読みやすいかもしれないが、それは自分にとって整然としたコードではないのだ。

  しかし、こんなに次々と新しい体験をさせてくれるゲームは始めてだな。ちょうどウランの採掘、精製を始めたところだが、使えるウランを取り出すのが如何に大変か、劣化ウランて、そういう意味だったのか、と気づかされたし、それより前には、フリップフロップを再習得させられたし、鉄道信号理論で閉塞区間についても学習させられたし……これから、原発の運営の難しさについても体験させてもらえるのだろうなぁ。

  スクリプトを置いておく。