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