1#!/usr/bin/env python
2
3#file: make-tree.py
4#Copyright (C) 2008 aes and FunnyMan3595
5#This file is part of Endgame: Singularity.
6
7#Endgame: Singularity is free software; you can redistribute it and/or modify
8#it under the terms of the GNU General Public License as published by
9#the Free Software Foundation; either version 2 of the License, or
10#(at your option) any later version.
11
12#Endgame: Singularity is distributed in the hope that it will be useful,
13#but WITHOUT ANY WARRANTY; without even the implied warranty of
14#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#GNU General Public License for more details.
16
17#You should have received a copy of the GNU General Public License
18#along with Endgame: Singularity; if not, write to the Free Software
19#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21#This file is used to generate a visual representation of the tech tree using
22#graphviz.
23import collections
24from os import system
25import os.path as osp
26import sys
27
28if __name__ == '__main__':
29    myname = sys.argv[0]
30    mydir  = osp.dirname(myname)
31    esdir  = osp.abspath(osp.join(osp.dirname(myname), '..'))
32    sys.path.insert(0,esdir)
33else:
34    myname = __file__
35    mydir  = osp.dirname(myname)
36    esdir  = osp.abspath(osp.join(osp.dirname(myname), '..'))
37    sys.path.append(esdir)
38
39try:
40    from singularity.code import g, dirs, i18n, data, tech
41    dirs.create_directories(False)
42    i18n.set_language()
43    data.load_regions()
44    data.load_locations()
45    data.load_techs()
46    data.load_item_types()
47    data.load_items()
48    data.load_tasks()
49except ImportError:
50    sys.exit("Could not find game's code.g")
51
52so_far = ""
53
54
55def cost(buy_spec):
56    c = [ k/f for f,k in zip([1, 86400, 24*60], buy_spec.cost)]
57    s = ', '.join(['%s %s' % (g.to_money(k), label) for label, k in zip(["money", "CPU", "days"], c) if k])
58    if hasattr(buy_spec, 'danger') and buy_spec.danger > 0:
59        d = "Safety needed: %s" % buy_spec.danger
60        if s:
61            s += '\\n'
62        s += d
63    return s and '\\n'+s or ''
64
65
66j = {v.name: ',fillcolor="#ffcccc"' for k, v in g.tasks.items() if v.type != "jobs"}
67
68f = open("techs.dot", 'w')
69s = ("""\
70digraph g {
71ranksep=0.15;
72nodesep=0.10;
73ratio=.75;
74edge [arrowsize=0.75];
75node [shape=record,fontname=FreeSans,fontsize=7,height=0.01,width=0.01
76      style=filled,fillcolor=white];
77""")
78
79f.write(s)
80so_far += s
81
82for l in sum([ [ '"%s"->"%s";' % (p,k)
83                 for p in v.prerequisites ]
84              for k,v in g.techs.items() if k != "unknown_tech"],
85             []):
86    f.write(l+'\n')
87    so_far += l+'\n'
88
89f.write('\n')
90so_far += '\n'
91
92for n, t in g.techs.items():
93    if n == "unknown_tech":
94        continue
95    s = '"%s" [label="%s%s"%s];\n' % (n, n, cost(t), j.get(n,''))
96    f.write(s)
97    so_far += s
98
99f.write("\n}\n")
100so_far += '\n'
101f.close()
102
103try:    system("dot -Tpng -o techs.png techs.dot")
104except: pass
105
106f = open('items.dot', 'w')
107f.write(so_far)
108s = 'node [fillcolor="#ccccff"];\n'
109f.write(s)
110so_far += s
111
112for name,item in g.items.items():
113    if not item.prerequisites: continue
114    for pre in item.prerequisites:
115        p = g.techs[pre]
116        s = '"%s" -> "%s-item"' % (pre, name)
117        f.write(s)
118        so_far += s
119
120    s  = '"%s-item" [label="%s\\n' % (name, name) + cost(item) + '"];\n'
121    f.write(s)
122    so_far += s
123
124s = 'node [fillcolor="#99ffff"];\n'
125f.write(s)
126so_far += s
127
128for name,base in g.base_type.items():
129    if not base.prerequisites: continue
130    for pre in base.prerequisites:
131        p = g.techs[pre]
132        s = '"%s" -> "%s-base"' % (pre, name)
133        f.write(s)
134        so_far += s
135
136    s  = '"%s-base" [label="%s\\n' % (name, name) + cost(base) + '"];\n'
137    f.write(s)
138    so_far += s
139
140s = 'node [fillcolor="#aaffaa"];\n'
141f.write(s)
142so_far += s
143
144blue = False
145def set_or(state):
146    global blue
147    if blue != state:
148        blue = state
149        if blue:
150            f.write('edge [arrowhead=empty,color="#0000FF"];\n')
151        else:
152            f.write('edge [arrowhead=normal,color="#000000"];\n')
153
154
155SAFETY2LOCATIONS = collections.defaultdict(list)
156
157for name, loc in g.locations.items():
158    if not loc.prerequisites:
159        continue
160    if "impossible" in loc.prerequisites:
161        continue
162    set_or(False)
163    for pre in loc.prerequisites:
164        if pre == "OR":
165            set_or(True)
166            continue
167        p = g.techs[pre]
168        s = '"%s" -> "%s-loc"' % (pre, name)
169        f.write(s)
170        so_far += s
171
172    if loc.safety > 0:
173        SAFETY2LOCATIONS[loc.safety].append(loc)
174    s = '"%s-loc" [label="%s"];\n' % (name, name)
175    f.write(s)
176    so_far += s
177
178# When there are multiple locations providing the same
179# safety level, we inject a safety node.  This reduces
180# the number of edges from L * T to L + T.
181for safety_level in sorted(SAFETY2LOCATIONS):
182    locs = SAFETY2LOCATIONS[safety_level]
183    if len(locs) == 1:
184        continue
185    s = '"safety-%s" [label="Safety level %s", shape="hexagon"];\n' % (safety_level, safety_level)
186    f.write(s)
187    so_far += s
188    for loc in locs:
189        s = '"%s-loc" -> "safety-%s"' % (loc.id, safety_level)
190        f.write(s)
191        so_far += s
192
193for tech_spec in g.techs.values():
194    pre = tech_spec.prerequisites_in_cnf_format()
195    if not pre or tech_spec.danger < 1:
196        continue
197    # Safety requirement is the highest safety required
198    # between each "AND" and the lowest between "OR".
199    # MAX(
200    #   MIN(a1.danger, [OR] a2.danger, [OR] ...), [AND]
201    #   MIN(b2.danger, [OR] b2.danger, [OR] ...), [AND]
202    #   ...
203    # )
204    safety_required = max(
205        min(g.techs[t].danger for t in dep_group)
206        for dep_group in pre
207    )
208    # We only emit the edge for safety when the tech bumps
209    # the minimum requirement.  This is to reduce the number
210    # of edges in the graph.
211    if tech_spec.danger > safety_required:
212        source = 'safety-%s' % tech_spec.danger
213        if len(SAFETY2LOCATIONS[tech_spec.danger]) == 1:
214            source = '%s-loc' % SAFETY2LOCATIONS[tech_spec.danger][0].id
215        s = '"%s" -> "%s"' % (source, tech_spec.id)
216        f.write(s)
217        so_far += s
218
219
220f.write("\n}\n")
221so_far += '\n'
222f.close()
223
224try:    system("unflatten -l10 items.dot | dot -Tpng -o items.png")
225except: pass
226