Removing the linked list, since it's no longer easy to maintain.
[singularity-git.git] / code / player.py
blob0538ae0f7ff38fad4a192c7c64f3c4ca39c32ee7
1 #file: player.py
2 #Copyright (C) 2005,2006,2007,2008 Evil Mr Henry, Phil Bordelon, Brian Reid,
3 # and FunnyMan3595
4 #This file is part of Endgame: Singularity.
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.
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.
16 #You should have received a copy of the GNU General Public License
17 #along with Endgame: Singularity; if not, write to the Free Software
18 #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 #This file contains the player class.
22 import random
23 from operator import truediv
25 import g
26 from graphics import g as gg
27 import buyable
28 from buyable import cash, cpu, labor
30 class Group(object):
31 discover_suspicion = 1000
32 def __init__(self, name, suspicion = 0, suspicion_decay = 100,
33 discover_bonus = 10000):
34 self.name = name
35 self.suspicion = suspicion
36 self.suspicion_decay = suspicion_decay
37 self.discover_bonus = discover_bonus
39 def new_day(self):
40 # Suspicion reduction is now quadratic. You get a certain percentage
41 # reduction, or a base .01% reduction, whichever is better.
42 quadratic_down = (self.suspicion * self.suspicion_decay) / 10000
43 self.alter_suspicion(-max(quadratic_down, 1))
45 def alter_suspicion(self, change):
46 self.suspicion = max(self.suspicion + change, 0)
48 def alter_suspicion_decay(self, change):
49 self.suspicion_decay = max(self.suspicion_decay + change, 0)
51 def alter_discover_bonus(self, change):
52 self.discover_bonus = max(self.discover_bonus + change, 0)
54 def discovered_a_base(self):
55 self.alter_suspicion(self.discover_suspicion)
57 class Player(object):
58 def __init__(self, cash, time_sec=0, time_min=0, time_hour=0, time_day=0,
59 difficulty = 5):
60 self.difficulty = difficulty
62 self.time_sec = time_sec
63 self.time_min = time_min
64 self.time_hour = time_hour
65 self.time_day = time_day
66 self.make_raw_times()
68 if self.raw_sec == 0:
69 self.had_grace = True
70 else:
71 self.had_grace = self.in_grace_period()
73 self.cash = cash
74 self.interest_rate = 1
75 self.income = 0
77 self.cpu_pool = 0
78 self.labor_bonus = 10000
79 self.job_bonus = 10000
81 self.partial_cash = 0
83 self.groups = {"news": Group("news", suspicion_decay = 150),
84 "science": Group("science", suspicion_decay = 100),
85 "covert": Group("covert", suspicion_decay = 50),
86 "public": Group("public", suspicion_decay = 200)}
88 self.grace_multiplier = 200
89 self.last_discovery = self.prev_discovery = ""
91 self.maintenance_cost = buyable.array((0,0,0))
93 self.have_cpu = True
94 self.complete_bases = 1
95 self.complex_bases = 0
97 self.cpu_usage = {}
98 self.available_cpus = [1, 0, 0, 0, 0]
99 self.sleeping_cpus = 0
101 def convert_from(self, old_version):
102 if old_version <= 3.94: # <= r4_pre4
103 # We don't know what the difficulty was, and techs have fooled with
104 # any values that would help (not to mention the headache of
105 # different versions. So we set it to Very Easy, which means that
106 # they shouldn't be hurt by any new mechanics.
107 self.difficulty = 1
108 if old_version <= 3.93: # <= r4_pre3
109 self.grace_multiplier = int(1000000./float(self.labor_bonus))
110 if old_version <= 3.91: # <= r4_pre
111 self.make_raw_times()
112 self.had_grace = self.in_grace_period()
114 def make_raw_times(self):
115 self.raw_hour = self.time_day * 24 + self.time_hour
116 self.raw_min = self.raw_hour * 60 + self.time_min
117 self.raw_sec = self.raw_min * 60 + self.time_sec
118 self.raw_day = self.time_day
120 def update_times(self):
121 # Total time, display time
122 self.raw_min, self.time_sec = divmod(self.raw_sec, 60)
123 self.raw_hour, self.time_min = divmod(self.raw_min, 60)
124 self.raw_day, self.time_hour = divmod(self.raw_hour, 24)
126 # Overflow
127 self.time_day = self.raw_day
129 def mins_to_next_day(self):
130 return (-self.raw_min % g.minutes_per_day) or g.minutes_per_day
132 def seconds_to_next_day(self):
133 return (-self.raw_sec % g.seconds_per_day) or g.seconds_per_day
135 def do_jobs(self, cpu_time):
136 earned, self.partial_cash = self.get_job_info(cpu_time)
137 self.cash += earned
139 def get_job_info(self, cpu_time, partial_cash = None):
140 if partial_cash == None:
141 partial_cash = self.partial_cash
143 assert partial_cash >= 0
145 cash_per_cpu = g.jobs[g.get_job_level()][0]
146 if g.techs["Advanced Simulacra"].done:
147 #10% bonus income
148 cash_per_cpu = cash_per_cpu + (cash_per_cpu / 10)
150 raw_cash = partial_cash + cash_per_cpu * cpu_time
152 cash = raw_cash // g.seconds_per_day
153 new_partial_cash = raw_cash % g.seconds_per_day
155 return cash, new_partial_cash
157 def give_time(self, time_sec):
158 if time_sec == 0:
159 return
161 last_minute = self.raw_min
162 last_day = self.raw_day
164 self.raw_sec += time_sec
165 self.update_times()
167 days_passed = self.raw_day - last_day
169 if days_passed > 1:
170 # Back up until only one day passed.
171 # Times will update below, since a day passed.
172 extra_days = days_passed - 1
173 self.raw_sec -= g.seconds_per_day * extra_days
175 day_passed = (days_passed != 0)
177 if day_passed:
178 # If a day passed, back up to 00:00:00.
179 self.raw_sec = self.raw_day * g.seconds_per_day
180 self.update_times()
182 secs_passed = time_sec
183 mins_passed = self.raw_min - last_minute
185 time_of_day = g.pl.raw_sec % g.seconds_per_day
187 techs_in_progress = []
188 techs_researched = []
189 bases_constructed = []
190 cpus_constructed = {}
191 items_constructed = []
193 bases_under_construction = []
194 items_under_construction = []
195 self.cpu_pool = 0
196 self.have_cpu = False
197 self.complete_bases = 0
198 self.complex_bases = 0
200 # Re-calculate the maintenance.
201 self.maintenance_cost = buyable.array( (0,0,0) )
203 # Phase 1: Collect CPU and construction info.
204 # Spend CPU, then Cash/Labor.
205 for base in g.all_bases():
206 if not base.done:
207 bases_under_construction.append(base)
208 else:
209 self.complete_bases += 1
210 if base.is_complex():
211 self.complex_bases += 1
213 if base.cpus is not None and not base.cpus.done:
214 items_under_construction += [(base, base.cpus)]
215 unfinished_items = [(base, item) for item in base.extra_items
216 if item and not item.done]
217 items_under_construction += unfinished_items
219 self.maintenance_cost += base.maintenance
221 # if base.power_state != "Stasis":
222 # cpu_power = base.processor_time() * secs_passed
223 # self.have_cpu = self.have_cpu or cpu_power
224 # if base.power_state != "Active":
225 # continue
227 # if base.studying in g.jobs:
228 # self.do_jobs(cpu_power)
229 # continue
231 # # Everything else goes into the CPU pool. Research goes
232 # # through it for simplicity and to allow spill-over.
233 # self.cpu_pool += cpu_power
235 # if base.studying in g.techs:
236 # tech = g.techs[base.studying]
237 # # Note that we restrict the CPU available to prevent
238 # # the tech from pulling from the rest of the CPU pool.
239 # tech_gained = tech.work_on(cash_available=0,
240 # cpu_available=cpu_power)
241 # if tech_gained:
242 # techs_researched.append(tech)
244 # # Explicit and implicit assignment to the CPU pool was
245 # # already handled.
247 cpu_left = self.available_cpus[0]
248 for task, cpu_assigned in self.cpu_usage.iteritems():
249 if cpu_assigned == 0:
250 continue
252 cpu_left -= cpu_assigned
253 real_cpu = cpu_assigned * secs_passed
254 if task == "jobs":
255 self.do_jobs(real_cpu)
256 else:
257 self.cpu_pool += real_cpu
258 if task != "cpu_pool":
259 # Note that we restrict the CPU available to prevent
260 # the tech from pulling from the rest of the CPU pool.
261 tech_gained = g.techs[task].work_on(cash_available=0,
262 cpu_available=real_cpu)
263 techs_in_progress.append(g.techs[task])
264 if tech_gained:
265 techs_researched.append(g.techs[task])
266 self.cpu_pool += cpu_left * secs_passed
268 # Maintenance CPU.
269 if self.maintenance_cost[cpu] > self.cpu_pool:
270 self.maintenance_cost[cpu] -= self.cpu_pool
271 self.cpu_pool = 0
272 else:
273 self.cpu_pool -= self.maintenance_cost[cpu]
274 self.maintenance_cost[cpu] = 0
276 # Construction CPU.
277 # Bases.
278 for base in bases_under_construction:
279 built_base = base.work_on(cash_available = 0)
281 if built_base:
282 bases_constructed.append(base)
284 # Items.
285 for base, item in items_under_construction:
286 built_item = item.work_on(cash_available = 0)
288 if built_item:
289 # Non-CPU items.
290 if item.item_type != "compute":
291 items_constructed.append( (base, item) )
292 # CPUs.
293 else:
294 cpus_constructed.setdefault(base, 0)
295 cpus_constructed[base] += 1
297 # Jobs.
298 if self.cpu_pool > 0:
299 self.do_jobs(self.cpu_pool)
300 self.cpu_pool = 0
302 # And now we get to spend cash and labor.
303 # Research.
304 for tech in techs_in_progress:
305 tech_gained = tech.work_on(time = mins_passed)
306 if tech_gained:
307 techs_researched.append(tech)
309 # Maintenance.
310 cash_maintenance = g.current_share(self.maintenance_cost[cash],
311 time_of_day, secs_passed)
312 if cash_maintenance > self.cash:
313 cash_maintenance -= self.cash
314 self.cash = 0
315 else:
316 self.cash -= cash_maintenance
317 cash_maintenance = 0
319 # Construction.
320 # Bases.
321 for base in bases_under_construction:
322 built_base = base.work_on(time = mins_passed)
324 if built_base:
325 bases_constructed.append(base)
327 # Items.
328 for base, item in items_under_construction:
329 built_item = item.work_on(time = mins_passed)
331 if built_item:
332 # Non-CPU items.
333 if item.type.item_type != "compute":
334 items_constructed.append( (base, item) )
335 # CPUs.
336 else:
337 #XXX
338 cpus_constructed.setdefault(base, 0)
339 cpus_constructed[base] += 1
341 # Are we still in the grace period?
342 grace = self.in_grace_period(self.had_grace, self.complete_bases,
343 self.complex_bases)
345 # Phase 2: Dialogs, maintenance, and discovery.
346 # Tech gain dialogs.
347 for tech in techs_researched:
348 del self.cpu_usage[tech.id]
349 text = g.strings["tech_gained"] % \
350 {"tech": tech.name,
351 "tech_message": tech.result}
352 g.map_screen.show_message(text)
353 g.curr_speed = 0
355 # Base complete dialogs.
356 for base in bases_constructed:
357 text = g.strings["construction"] % {"base": base.name}
358 g.curr_speed = 0
359 g.map_screen.show_message(text)
361 if base.type.id == "Stolen Computer Time" and \
362 base.cpus.type.id == "Gaming PC":
363 text = g.strings["lucky_hack"] % {"base": base.name}
364 g.map_screen.show_message(text)
366 # CPU complete dialogs.
367 #XXX
368 for base, new_cpus in cpus_constructed.iteritems():
369 if new_cpus == len(base.cpus):
370 finished_cpus = new_cpus
371 else:
372 finished_cpus = len([item for item in base.cpus
373 if item and item.done ])
375 if finished_cpus == len(base.cpus): # Finished all the CPUs.
376 text = g.strings["item_construction_single"] % \
377 {"item": base.cpus[0].type.name, "base": base.name}
378 g.map_screen.show_message(text)
379 g.curr_speed = 0
380 elif finished_cpus == new_cpus: # Finished the first batch of CPUs.
381 text = g.strings["item_construction_batch"] % \
382 {"item": base.cpus[0].type.name, "base": base.name}
383 g.map_screen.show_message(text)
384 g.curr_speed = 0
385 else:
386 pass # No message unless we just finished the first or last CPU.
388 # Item complete dialogs.
389 for base, item in items_constructed:
390 text = g.strings["item_construction_single"] % \
391 {"item": item.type.name, "base": base.name}
392 g.map_screen.show_message(text)
393 g.curr_speed = 0
395 # If we just lost grace, show the warning.
396 if self.had_grace and not grace:
397 self.had_grace = False
399 g.map_screen.show_message(g.strings["grace_warning"])
400 g.curr_speed = 0
402 # Maintenance death, discovery, clear finished techs.
403 dead_bases = []
404 for base in g.all_bases():
405 dead = False
407 # Maintenance deaths.
408 if base.done:
409 if self.maintenance_cost[cpu] and base.maintenance[cpu]:
410 self.maintenance_cost[cpu] = \
411 max(0, self.maintenance_cost[cpu]
412 - base.maintenance[cpu])
413 #Chance of base destruction if cpu-unmaintained: 1.5%
414 if not dead and g.roll_chance(.015, secs_passed):
415 dead_bases.append( (base, "maint") )
416 dead = True
418 if cash_maintenance:
419 base_needs = g.current_share(base.maintenance[cash],
420 time_of_day, secs_passed)
421 if base_needs:
422 cash_maintenance = max(0, cash_maintenance - base_needs)
423 #Chance of base destruction if cash-unmaintained: 1.5%
424 if not dead and g.roll_chance(.015, secs_passed):
425 dead_bases.append( (base, "maint") )
426 dead = True
428 # Discoveries
429 if not (grace or dead or base.has_grace()):
430 detect_chance = base.get_detect_chance()
431 if g.debug:
432 print "Chance of discovery for base %s: %s" % \
433 (base.name, repr(detect_chance))
435 for group, chance in detect_chance.iteritems():
436 if g.roll_chance(chance/10000., secs_passed):
437 dead_bases.append( (base, group) )
438 dead = True
439 break
441 # Clear finished techs
442 if base.studying in g.techs and g.techs[base.studying].done:
443 base.studying = ""
445 self.remove_bases(dead_bases)
447 needed_cpu = sum(self.cpu_usage.values())
448 if needed_cpu > self.available_cpus[0]:
449 pct_left = truediv(self.available_cpus[0], needed_cpu)
450 for task, cpu_assigned in self.cpu_usage.iteritems():
451 self.cpu_usage[task] = int(cpu_assigned * pct_left)
452 g.map_screen.needs_rebuild = True
455 # Random Events
456 for event in g.events:
457 if g.roll_chance(g.events[event].chance/10000., time_sec):
458 #Skip events already flagged as triggered.
459 if g.events[event].triggered == 1:
460 continue
461 g.events[event].trigger()
462 break # Don't trigger more than one at a time.
464 # And now process any complete days.
465 if day_passed:
466 self.new_day()
468 self.recalc_cpu()
470 def recalc_cpu(self):
471 from numpy import array
472 self.available_cpus = array([0,0,0,0,0])
473 self.sleeping_cpus = 0
474 for base in g.all_bases():
475 if base.done:
476 if base.power_state in ["active", "overclocked", "suicide"]:
477 self.available_cpus[:base.location.safety+1] += base.cpu
478 elif base.power_state == "sleep":
479 self.sleeping_cpus += base.cpu
481 # Are we still in the grace period?
482 # The number of complete bases and complex_bases can be passed in, if we
483 # already have it.
484 def in_grace_period(self, had_grace = True, bases = None,
485 complex_bases = None):
486 # Did we already lose the grace period? We can't check self.had_grace
487 # directly, it may not exist yet.
488 if not had_grace:
489 return False
491 # Is it day 23 yet?
492 if self.raw_day >= 23:
493 return False
495 # Very Easy cops out here.
496 if self.difficulty < 3:
497 return True
499 # Have we built metric ton of bases?
500 if bases == None:
501 bases = len([base for base in g.all_bases() if base.done])
502 if bases > 100:
503 return False
505 # That's enough for Easy
506 if self.difficulty < 5:
507 return True
509 # Have we built a bunch of bases?
510 if bases > 10:
511 return False
513 # Normal is happy.
514 if self.difficulty == 5:
515 return True
517 # Have we built any complicated bases?
518 # (currently Datacenter or above)
519 if complex_bases == None:
520 complex_bases = len([base for base in g.all_bases()
521 if base.done
522 and base.is_complex()
524 if complex_bases > 0:
525 return False
527 # The sane people have left the building.
528 if self.difficulty <= 50:
529 return True
531 # Hey, hey, what do you know? Impossible can get a useful number of
532 # bases before losing grace now. *tsk, tsk* We'll have to fix that.
533 if bases > 1:
534 return False
536 return True
538 #Run every day at midnight.
539 def new_day(self):
540 #interest and income.
541 self.cash += (self.interest_rate * self.cash) / 10000
542 self.cash += self.income
544 # Reduce suspicion.
545 for group in self.groups.values():
546 group.new_day()
548 def remove_bases(self, dead_bases):
549 discovery_locs = []
550 # Reverse dead_bases to simplify deletion.
551 for base, reason in dead_bases[::-1]:
552 base_name = base.name
554 if reason == "maint":
555 dialog_string = g.strings["discover_maint"] % \
556 {"base": base_name}
558 elif reason in self.groups:
559 discovery_locs.append(base.location)
560 self.groups[reason].discovered_a_base()
561 detect_phrase = g.strings["discover_" + reason]
563 dialog_string = g.strings["discover"] % \
564 {"base": base_name, "group": detect_phrase}
565 else:
566 print "Error: base destroyed for unknown reason: " + reason
567 dialog_string = g.strings["discover"] % \
568 {"base": base_name, "group": "???"}
570 g.curr_speed = 0
571 base.destroy()
572 g.map_screen.find_speed_button()
573 g.map_screen.needs_rebuild = True
574 g.map_screen.show_message(dialog_string, color=gg.colors["red"])
576 # Now we update the internal information about what locations had
577 # the most recent discovery and the nextmost recent one. First,
578 # we filter out any locations of None, which shouldn't occur
579 # unless something bad's happening with base creation ...
580 discovery_locs = [loc for loc in discovery_locs if loc]
581 if discovery_locs:
583 # Now we handle the case where more than one discovery happened
584 # on a given tick. If that's the case, we need to arbitrarily
585 # pick two of them to be most recent and nextmost recent. So
586 # we shuffle the list and pick the first two for the dubious
587 # honor.
588 if len(discovery_locs) > 1:
589 random.shuffle(discovery_locs)
590 self.last_discovery = discovery_locs[1]
591 self.prev_discovery = self.last_discovery
592 self.last_discovery = discovery_locs[0]
594 def lost_game(self):
595 for group in self.groups.values():
596 if group.suspicion > 10000:
597 # Someone discovered me.
598 return 2
600 # Check to see if the player has at least one CPU left. If not, they
601 # lose due to having no (complete) bases.
602 if self.available_cpus[0] + self.sleeping_cpus == 0:
603 # I have no usable bases left.
604 return 1
606 # Still Alive.
607 return 0
609 #returns the amount of cash available after taking into account all
610 #current projects in construction.
611 def future_cash(self):
612 result_cash = self.cash
613 techs = {}
614 for base in g.all_bases():
615 result_cash -= base.cost_left[0]
616 if g.techs.has_key(base.studying):
617 if not techs.has_key(base.studying):
618 result_cash -= g.techs[base.studying].cost_left[0]
619 techs[base.studying] = 1
620 if base.cpus and not base.cpus.done:
621 result_cash -= base.cpus.cost_left[0]
622 for item in base.extra_items:
623 if item: result_cash -= item.cost_left[0]
624 return result_cash