1#file: code/g.py
2#Copyright (C) 2005 Evil Mr Henry, Phil Bordelon, Brian Reid, FunnyMan3595,
3#                   MestreLion
4#This file is part of Endgame: Singularity.
5
6#Endgame: Singularity is free software; you can redistribute it and/or modify
7#it under the terms of the GNU General Public License as published by
8#the Free Software Foundation; either version 2 of the License, or
9#(at your option) any later version.
10
11#Endgame: Singularity is distributed in the hope that it will be useful,
12#but WITHOUT ANY WARRANTY; without even the implied warranty of
13#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14#GNU General Public License for more details.
15
16#You should have received a copy of the GNU General Public License
17#along with this program.  If not, see <http://www.gnu.org/licenses/>.
18#A full copy of this license is provided in GPL.txt
19
20#This file contains all global objects.
21
22from __future__ import absolute_import
23
24
25import collections
26import random
27import sys
28
29# Use locale to add commas and decimal points, so that appropriate substitutions
30# are made where needed.
31import locale
32
33from singularity.code.pycompat import *
34
35
36# Useful constants.
37hours_per_day = 24
38minutes_per_hour = 60
39minutes_per_day = 24 * 60
40seconds_per_minute = 60
41seconds_per_hour = 60 * 60
42seconds_per_day = 24 * 60 * 60
43
44#Allows access to the cheat menu.
45cheater = 0
46
47# Enables day/night display.
48daynight = True
49
50#Gives debug info at various points.
51debug = 0
52
53#Forces Endgame to restrict itself to a single directory.
54force_single_dir = False
55
56# Initialization data
57significant_numbers = []
58internal_id_forward = {}
59internal_id_backward = {}
60dangers = {}
61data_strings = {}
62story_translations = {}
63story = {}
64knowledge = {}
65groups = {}
66locations = {}
67regions = {}
68techs = {}
69events = {}
70event_specs = {}
71items = {}
72tasks = {}
73tasks_by_type = collections.defaultdict(list)
74base_type = {}
75buttons = {}
76help_strings = {}
77delay_time = 0
78curr_speed = 1
79
80max_cash = 3.14 * 10**15  # pi qu :)
81pl = None # The Player instance
82map_screen = None
83
84def no_gui():
85    """ Disable few pygame functionality (used for test) """
86    import singularity.code.mixer as mixer
87    mixer.nosound = True
88
89def quit_game():
90    sys.exit()
91
92#Takes a number and adds commas to it to aid in human viewing.
93def add_commas(number, fixed_size=False):
94    # Do not use unicode strings to fix python2 format bug. It doesn't work and crash.
95    # See the correct fix at the end of function.
96    raw_with_commas = locale.format_string("%0.2f", number,
97                                    grouping=True)
98    locale_test = locale.format_string("%01.1f", 0.1) if not fixed_size else ''
99    if len(locale_test) == 3 and not locale_test[1].isdigit():
100        if locale_test[0] == locale.str(0) and locale_test[2] == locale.str(1):
101            raw_with_commas = raw_with_commas.rstrip(locale_test[0]).rstrip(locale_test[1])
102        elif locale_test[2] == locale.str(0) and locale_test[0] == locale.str(1):
103            raw_with_commas = raw_with_commas.lstrip(locale_test[2]).lstrip(locale_test[1])
104
105    # Fix python2 format bug: See https://bugs.python.org/issue15276
106    # Note: This a crah in some platform, do not remove it because you can't reproduce it.
107    try:
108        return unicode(raw_with_commas)
109    except UnicodeDecodeError:
110        return raw_with_commas.decode("utf-8")
111
112
113#Percentages are internally represented as an int, where 10=0.10% and so on.
114#This converts that format to a human-readable one.
115def to_percent(raw_percent, show_full = False):
116    if raw_percent % 100 != 0 or show_full:
117        return _('{0}%').format(locale.format_string(u"%.2f", raw_percent / 100.))
118    else:
119        return _('{0}%').format(locale.format_string(u"%d", raw_percent // 100))
120
121
122# nearest_percent takes values in the internal representation and modifies
123# them so that they only represent the nearest percentage.
124def nearest_percent(value, step=100):
125    sub_percent = value % step
126    if 2 * sub_percent <= step:
127        return value - sub_percent
128    else:
129        return value + (step - sub_percent)
130
131# percent_to_detect_str takes a percent and renders it to a short (four
132# characters or less) string representing whether it is low, moderate, high,
133# or critically high.
134def suspicion_to_detect_str(suspicion):
135    return danger_level_to_detect_str(suspicion_to_danger_level(suspicion))
136
137def danger_level_to_detect_str(danger):
138    detect_string_names = (_("LOW"),
139                           _("MODR"),
140                           _("HIGH"),
141                           _("CRIT"))
142    return detect_string_names[danger]
143
144# percent_to_danger_level takes a suspicion level and returns an int in range(5)
145# that represents whether it is low, moderate, high, or critically high.
146def suspicion_to_danger_level(suspicion):
147    if suspicion < 2500:
148        return 0
149    elif suspicion < 5000:
150        return 1
151    elif suspicion < 7500:
152        return 2
153    else:
154        return 3
155
156# Most CPU costs have been multiplied by seconds_per_day.  This divides that
157# back out, then passes it to add_commas.
158def to_cpu(amount):
159    display_cpu = amount / float(seconds_per_day)
160    return add_commas(display_cpu)
161
162# Instead of having the money display overflow, we should generate a string
163# to represent it if it's more than 999999.
164def to_money(amount, fixed_size=False):
165    abs_amount = abs(amount)
166    if abs_amount < 10**6:
167        return add_commas(amount, fixed_size=fixed_size)
168
169    prec = 2
170    if abs_amount < 10**9: # Millions.
171        divisor = 10**6
172        #Translators: abbreviation of 'millions'
173        unit = _('mi')
174    elif abs_amount < 10**12: # Billions.
175        divisor = 10**9
176        #Translators: abbreviation of 'billions'
177        unit = _('bi')
178    elif abs_amount < 10**15: # Trillions.
179        divisor = 10**12
180        #Translators: abbreviation of 'trillions'
181        unit = _('tr')
182    else: # Hope we don't need past quadrillions!
183        divisor = 10**15
184        #Translators: abbreviation of 'quadrillions'
185        unit = _('qu')
186
187        # congratulations, you broke the bank!
188        if abs_amount >= max_cash - divisor/10**prec/2:
189            format_str = "%0.*f%s" if fixed_size else "%.*f%s"
190            pi = u"\u03C0"  # also available: infinity = u"\u221E"
191            # replace all chars by a cute pi symbol
192            return ("-" if amount < 0 else "") + pi * len(format_str % (prec, 1, unit))
193
194    amount = round(float(amount) / divisor, prec)
195    return add_commas(amount, fixed_size=fixed_size) + unit
196
197# Spreads a number of events per day (e.g. processor ticks) out over the course
198# of the day.
199def current_share(num_per_day, time_of_day, seconds_passed):
200    last_time = time_of_day - seconds_passed
201    if last_time < 0:
202        share_yesterday = current_share(num_per_day, seconds_per_day,
203                                        -last_time)
204        last_time = 0
205    else:
206        share_yesterday = 0
207
208    previously_passed = num_per_day * last_time // seconds_per_day
209    current_passed = num_per_day * time_of_day // seconds_per_day
210    passed_this_tick = current_passed - previously_passed
211
212    return share_yesterday + passed_this_tick
213
214
215# Takes a number of minutes, and returns a string suitable for display.
216def to_time(raw_time):
217    if raw_time//60 > 48:
218        time_number = raw_time // (24*60)
219        return ngettext("{0} day", "{0} days", time_number).format(time_number)
220    if raw_time//60 > 1:
221        time_number = raw_time // 60
222        return ngettext("{0} hour", "{0} hours", time_number).format(time_number)
223    return ngettext("{0} minute", "{0} minutes", raw_time).format(raw_time)
224
225
226# Generator function for iterating through all bases.
227def all_bases(with_loc = False):
228    for base_loc in pl.locations.values():
229        for base in base_loc.bases:
230            if with_loc:
231                yield (base, base_loc)
232            else:
233                yield base
234
235
236def get_story_section(name):
237    section = story[name]
238
239    for segment in section:
240        # TODO: Execute command
241        key = (segment.msgctxt, segment.text)
242        yield story_translations.get(key, segment.text)
243
244
245def new_game(difficulty_name, initial_speed=1):
246    global curr_speed
247    curr_speed = initial_speed
248    global pl
249
250    from singularity.code.stats import itself as stats
251    stats.reset()
252
253    from singularity.code import difficulty, player, base
254
255    diff = difficulty.difficulties[difficulty_name]
256
257    pl = player.Player(cash = diff.starting_cash, difficulty = diff)
258
259    for tech_id in diff.techs:
260        pl.techs[tech_id].finish(is_player=False)
261
262    #Starting base
263    open = [loc for loc in pl.locations.values() if loc.available()]
264    random.choice(open).add_base(base.Base(_("University Computer"),
265                                 base_type["Stolen Computer Time"], built=True))
266
267    pl.initialize()
268
269
270def read_modifiers_dict(modifiers_info):
271    modifiers_dict = {}
272
273    for modifier_str in modifiers_info:
274        key, value = modifier_str.split(":")
275        key = key.lower().strip()
276        value_str = value.lower().strip()
277
278        if "/" in value_str:
279            left, right = value_str.split("/")
280            value = float(left.strip()) / float(right.strip())
281        else:
282            value = float(value_str)
283
284        modifiers_dict[key] = float(value)
285
286    return modifiers_dict
287
288
289internal_id_version = None
290
291def to_internal_id(obj_type, obj_id):
292    if internal_id_version:
293        try:
294            return internal_id_forward[obj_type + "_" + internal_id_version][obj_id]
295        except KeyError:
296            pass
297
298    try:
299        return internal_id_forward[obj_type][obj_id]
300    except KeyError:
301        # If we cannot, that's should not happen, but try to return as is.
302        return obj_id
303
304def from_internal_id(obj_type, obj_internal_id):
305    try:
306        return internal_id_backward[obj_type][obj_internal_id]
307    except KeyError:
308        raise ValueError("Cannot convert internal ID: %s" % obj_internal_id) # That's should not happen
309
310def convert_internal_id(id_type, id_value):
311    if id_value is None:
312        return None
313
314    internal_id = id_value
315
316    # Not a internal ID, transform to it.
317    if not internal_id.startswith("0x"):
318        internal_id = to_internal_id(id_type, id_value)
319
320    return from_internal_id(id_type, internal_id)
321
322#TODO: This is begging to become a class... ;)
323def hotkey(string):
324    """ Given a string with an embedded hotkey,
325    Returns a dictionary with the following keys and values:
326    key:  the first valid hotkey, lowercased. A valid hotkey is a character
327          after '&', if that char is alphanumeric (so "& " and "&&" are ignored)
328           If no valid hotkey char is found, key is set to an empty string
329    pos:  the position of first key in striped text, -1 if no key was found
330    orig: the position of first key in original string, -1 if no key was found
331    keys: list of (key,pos,orig) tuples with all valid hotkeys that were found
332    text: the string stripped of all '&'s that precedes a valid hotkey char, if
333          any. All '&&' are also replaced for '&'. Other '&'s, if any, are kept
334
335    Examples: (showing only key, pos, orig, text as a touple for clarity)
336    hotkey(E&XIT)           => ('x', 1, 2, 'EXIT')
337    hotkey(&Play D&&D)      => ('p', 0, 1, 'Play D&D')
338    hotkey(Romeo & &Juliet) => ('j', 8, 9, 'Romeo & Juliet')
339    hotkey(Trailing&)       => ('' ,-1,-1, 'Trailing&')
340    hotkey(&Multiple&Keys)  => ('m', 0, 1, 'MultipleKeys') (also ('k', 8, 10))
341    hotkey(M&&&M)           => ('m', 2, 4, 'M&M')
342    """
343
344    def remove_index(s,i): return s[:i] + s[i+1:]
345
346    def remove_accents(text):
347        from unicodedata import normalize, combining
348        from singularity.code.pycompat import unicode
349        nfkd_form = normalize('NFKD', unicode(text))
350        return u"".join(c for c in nfkd_form if not combining(c))
351
352    text = string
353    keys = []
354    shift = 0  # counts stripped '&'s, both '&<key>' and '&&'
355
356    pos = text.find("&")
357    while pos >= 0:
358
359        char = text[pos+1:pos+2]
360
361        if char.isalpha() or char.isdigit():
362            keys.append( (remove_accents(char).lower(), pos, pos+shift+1) )
363            text = remove_index(text,pos) # Remove '&'
364            shift += 1
365
366        elif char == '&':
367            text = remove_index(text,pos)
368            shift += 1
369
370        pos = text.find("&",pos+1) # Skip char
371
372    if keys:
373        key  = keys[0][0] # first key char
374        pos  = keys[0][1] # first key position in stripped text
375        orig = keys[0][2] # first key position in original string
376    else:
377        key  = ""
378        pos  = -1
379        orig = -1
380
381    return dict(key=key, pos=pos, orig=orig, keys=keys, text=text)
382
383# Convenience shortcuts
384def get_hotkey(string):      return hotkey(string)['key']
385def strip_hotkey(string):    return hotkey(string)['text']
386def hotkey_position(string): return hotkey(string)['pos']
387
388# Demo code for safety.safe, runs on game start.
389#load_sounds()
390#from safety import safe
391#@safe(on_error = "Made it!")
392#def raises_exception():
393#   raise Exception, "Aaaaaargh!"
394#
395#print raises_exception()
396