はじめに
Satisfactoryというゲームをご存じでしょうか?
このゲームでは工場を作って、大量の部品を生産していきます。しかし、そのレシピ(製造フロー)が非常に複雑で、何をどのくらい作ればいいのかを考えるのが大変です。
グラフで可視化すれば解決できるのでは?
そこで、レシピをグラフ(ネットワーク)で可視化すること解決を図りました。グラフはノード(頂点)とエッジ(辺)で構成されるデータ構造です。ネットワークとも言います。グラフを可視化することで、複雑なデータの関係性を直感的に理解しやすくなります。今回は、ノードをアイテム、エッジを「材料かどうか」の関係とし、レシピをツリー状に可視化しました。
実際に作成したグラフが、冒頭の図です。階層型の構造にすることで、どのアイテムがどの材料から作られるのかが一目で分かります。また、エッジの上には「1分間に何個生産すればよいか」を表示しました。こうすることでレシピと必要生産量が簡単に確認できます。
実装
レシピのデータを取得する
まず、グラフの元となるレシピデータを用意する必要があります。
手作業でレシピを入力するのは大変なので、攻略Wikiからデータを抽出します。
https://satisfactory.wiki.gg/wiki/Recipes
このページには、ゲーム内のすべてのレシピが表形式で掲載されています。
この表を取得し、プログラムで扱いやすいデータに変換していきます。
ページの取得には requests、HTML解析には BeautifulSoup を使用します。
pip install requests beautifulsoup4
まずは、WikiのページのHTMLを取得します。
import requests
url = "https://satisfactory.wiki.gg/wiki/Recipes"
response = requests.get(url)
html_content = response.content
次に、HTMLを解析して、レシピの表を取り出します。
from bs4 import BeautifulSoup
# BeautifulSoupでHTMLを解析
soup = BeautifulSoup(html_content, "html.parser")
# 最初の<table>タグを取得
table = soup.find("table")
# 行データを取り出す
rows = table.find_all("tr")
data = []
# 各行のセル(thやtd)を取得
for row in rows:
cells = row.find_all(["th", "td"]) # th: ヘッダー, td: データセル
data.append(cells)
この時点で、HTMLの表をプログラムで扱えるリストの形に変換できました。
しかし、このままではHTMLのタグ情報が混ざっているため、次にデータを整理していきます。
import re
from bs4 import Tag
def extract_number(text: str) -> float:
# 文字列から数字部分だけを抽出
# 正規表現で数字とカンマを抽出
number_str = ''.join(re.findall(r'[0-9,]+', text))
# カンマを取り除き、floatに変換
return float(number_str.replace(',', ''))
def extract_materials_from_tag(tag: Tag) -> list:
# HTMLタグから材料情報をリストとして取得
recipe_items = tag.find_all('div', class_='recipe-item')
materials = []
for item in recipe_items:
# 数量を取得
quantity_text = item.find('span', class_='item-amount').text.strip()
quantity = extract_number(quantity_text)
# アイテム名を取得
name = item.find('span', class_='item-name').text.strip()
# 速度を取得 (数値部分のみ)
speed_text = item.find('span', class_='item-minute').text.strip()
speed = float(speed_text.split(' ')[0].replace(',', '')) # "1,500 / min" -> "1500" -> 1500.0
materials.append({
"name": name,
"quantity": quantity,
"speed": speed # 分速
})
return materials
def extract_produces_in(tag: Tag):
# 生産設備情報を取得
machine, crafting_time = list(tag.stripped_strings)[:2] # 一番最初のものだけ
crafting_time = extract_number(crafting_time)
produced_in = {
"name": machine,
"crafting_time": crafting_time # 秒
}
return produced_in
def parse_row(row: list[Tag]):
# 1つのレシピを解析し、辞書形式で返す
recipe, ingredients, produced_in, products, unlocked_by = row
return {
"recipe": list(recipe.stripped_strings)[0],
"ingredients": extract_materials_from_tag(ingredients),
"produced_in": extract_produces_in(produced_in),
"products": extract_materials_from_tag(products),
"unlocked_by": unlocked_by.get_text(separator=" ")
}
recipes_tag = data[1:] # 見出し行を除外
recipes = [parse_row(recipe_tag) for recipe_tag in recipes_tag]
# 確認
print(recipes[0])
出力例(見やすいように整形しています):
{
"recipe": "AI Expansion Server",
"ingredients": [
{"name": "Magnetic Field Generator", "quantity": 1.0, "speed": 4.0},
{"name": "Neural-Quantum Processor", "quantity": 1.0, "speed": 4.0},
{"name": "Superposition Oscillator", "quantity": 1.0, "speed": 4.0},
{"name": "Excited Photonic Matter", "quantity": 25.0, "speed": 100.0}
],
"produced_in": {"name": "Quantum Encoder", "crafting_time": 15.0},
"products": [
{"name": "AI Expansion Server", "quantity": 1.0, "speed": 4.0},
{"name": "Dark Matter Residue", "quantity": 25.0, "speed": 100.0}
],
"unlocked_by": "Tier 9 - Quantum Encoding\n"
}
レシピデータを辞書型に整理し、JSONに保存します。
アイテム名をキーにして辞書に変換すると、検索や処理がしやすくなります。
# "recipe"をキーとして、残りの情報を値にする辞書を作成
recipes_dict = {recipe["recipe"]: {key: recipe[key] for key in recipe if key != "recipe"} for recipe in recipes}
JSONファイルとして保存。
import json
with open("recipes.json", 'w', encoding='utf-8') as f:
json.dump(recipes_dict, f, ensure_ascii=False, indent=4)
次は、これをもとにグラフを生成していきます。
Networkx,Graphvizのインストール
グラフの作成にはNetworkXというライブラリを使用します。
pip install networkx
また、グラフの描画にはGraphvizを使います。NetworkXでも描画は可能ですが、階層型の描画にはGraphvizが必要です。
GraphvizはPythonライブラリではなく、システムにインストールする必要があります。
- Graphvizの公式サイト からインストーラーをダウンロード
- インストール時に 「Add Graphviz to the system PATH for current user」 にチェックを入れる
インストールが完了したら、Python用のGraphvizライブラリをインストールします。
pip install graphviz
グラフの作成・描画
保存しておいたレシピデータを読み込みます。
import json
# JSONファイルを読み込んで辞書に変換
with open("recipes.json", "r", encoding="utf-8") as f:
recipes = json.load(f)
グラフを作成する関数です。
import graphviz
import networkx as nx
def get_ingredients_rate_tree(G, target, target_rate):
"""
指定したアイテムを1分間にtarget_rate個作るために必要な材料の量を計算し、
networkxのグラフに追加していく。
引数:
G (networkx.Graph): 作成するグラフオブジェクト
target (str): 作成したいアイテム名
target_rate (float): 目標の生産速度(1分間に何個作るか)
"""
# レシピに target が存在しない場合は処理を終了
if target not in recipes:
return
# 作成倍率を計算
mul = target_rate / recipes[target]["products"][0]["speed"]
# 必要な材料ごとに処理
for ingredient in recipes[target]["ingredients"]:
# 必要な材料の量を計算
weight = ingredient["speed"] * mul
ingredient_name = ingredient["name"]
if G.has_edge(ingredient_name, target):
# すでに同じエッジが存在する場合、重みを加算
G[ingredient_name][target]['weight'] += weight
else:
# 新しいエッジとして追加(材料 -> 完成品 の流れ)
G.add_weighted_edges_from([[ingredient_name, target, weight]])
# 材料を再帰的に処理し、ツリー構造を作成
get_ingredients_rate_tree(G, ingredient_name, weight)
グラフを描画する関数です。
def draw_graph(G, filename="gv"):
# グラフをGraphvizを使って描画
dot = graphviz.Digraph(format="svg", filename=filename) # svgの方が文字が見やすい
dot.attr(rankdir="LR") # ツリーを横向き(左から右)に描画
# ノードを追加
for node in G.nodes():
dot.node(node, shape="box")
# エッジを追加(重みをラベルとして表示)
for u, v, data in G.edges(data=True):
weight = f"{data['weight']:.2f}" # エッジの重みを取得(小数点2桁に整形)
dot.edge(u, v, label=weight) # エッジのラベルに重みを設定
# ちなみにパラメータは文字列で指定する必要がある
dot.view()
実際にグラフを作成・描画
例:Adaptive Control Unit
を1分間に1個作るためのグラフを描画。
G = nx.DiGraph()
get_ingredients_rate_tree(G, "Adaptive Control Unit", 1)
draw_graph(G)
辺の上にある数字が1分間に必要な量です。
このように、レシピと必要な材料の関係が可視化されます。また、各材料の1分間あたりの必要量も表示されます。
ちなみに、ExecutableNotFound: failed to execute WindowsPath('dot'), make sure the Graphviz executables are on your systems' PATH
というエラーが出たら、エディタを再起動すると直るかもしれません。
終わりに
グラフで可視化することで、複雑なレシピを把握しやすくできました。これで計画が立てやすくなります。
ちなみに、作った後に調べてみたらもうすでに同じことをやっているWebアプリがありました。
https://www.satisfactorytools.com/1.0/production
車輪の再発明...