-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathsimulation.py
519 lines (484 loc) · 28 KB
/
simulation.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
import sys
import time
import datetime
from business import *
from config import Config
from town import *
from drama import StoryRecognizer
class Simulation(object):
"""A simulation instance."""
def __init__(self):
"""Initialize a Simulation object."""
# Load config parameters
self.config = Config()
# This gets incremented each time a new person is born/generated,
# which affords a persistent ID for each person
self.current_person_id = 0
self.current_place_id = 0
self.year = self.config.date_worldgen_begins[0]
self.true_year = self.config.date_worldgen_begins[0] # True year never gets changed during retconning
self.ordinal_date = datetime.date(*self.config.date_worldgen_begins).toordinal() # Days since 01-01-0001
self.month = datetime.date(*self.config.date_worldgen_begins).month
self.day = datetime.date(*self.config.date_worldgen_begins).day
self.ordinal_date_that_worldgen_ends = (
datetime.date(*self.config.date_worldgen_ends).toordinal()
)
self.time_of_day = "day"
self.date = self.get_date()
self.town = None
# Prepare a listing of all simulated events, which will facilitate debugging later
self.events = []
# A simulation's event number allows the precise ordering of events that
# happened on the same timestep -- every time an event happens, it requests an
# event number from Simulation.assign_event_number(), which also increments the running counter
self.event_number = -1
# Prepare a listing of all people born on each day -- this is used to
# age people on their birthdays; we start with (2, 29) initialized because
# we need to perform a check every March 1 to ensure that all leap-year babies
# celebrate their birthday that day on non-leap years
self.birthdays = {(2, 29): set()}
# Prepare a number that will hold a single random number that is generated daily -- this
# facilitates certain things that should be determined randomly but remain constant across
# a timestep, e.g., whether a person locked their door before leaving home
self.random_number_this_timestep = random.random()
self.weather = None
# Keep track of some metadata about timesteps that have actually been simulated
self.last_simulated_day = self.ordinal_date
self.n_simulated_timesteps = 0
# Prepare a story recognizer -- this a module whose job is to excavate nuggets of dramatic
# intrigue from the raw emergent material generated by this simulation
self.story_recognizer = StoryRecognizer(simulation=self)
@property
def random_person(self):
"""Return a random person living in the town of this simulation instance."""
return random.choice(list(self.town.residents))
@property
def random_company(self):
"""Return a random company in the town of this simulation instance."""
return random.choice(list(self.town.companies))
def recent_events(self):
"""Pretty-print the last five simulated events (for debugging purposes)."""
for recent_event in self.events[-5:]:
print recent_event
def establish_setting(self):
"""Establish the town that will be simulated."""
# Generate a town plan with at least two tracts
print "Generating a town..."
time.sleep(0.7)
self.town = Town(self)
while len(self.town.tracts) < 2:
self.town = Town(self)
# Have families establish farms on all of the town tracts except one,
# which will be a cemetery
for i in xrange(len(self.town.tracts)-2):
farmer = PersonExNihilo(sim=self, job_opportunity_impetus=Farmer, spouse_already_generated=None)
Farm(owner=farmer)
# farmer.move_into_the_town(hiring_that_instigated_move=farmer.occupation) # SHOULD BE ABLE TO DELETE THIS
# For the last tract, potentially have a quarry or coal mine instead of a farm
if random.random() < self.config.chance_of_a_coal_mine_at_time_of_town_founding:
owner = PersonExNihilo(sim=self, job_opportunity_impetus=Owner, spouse_already_generated=None)
CoalMine(owner=owner)
self.town.mayor = owner # TODO actual mayor stuff
elif random.random() < self.config.chance_of_a_quarry_at_time_of_town_founding:
owner = PersonExNihilo(sim=self, job_opportunity_impetus=Owner, spouse_already_generated=None)
Quarry(owner=owner)
self.town.mayor = owner # TODO actual mayor stuff
else:
farmer = PersonExNihilo(sim=self, job_opportunity_impetus=Farmer, spouse_already_generated=None)
Farm(owner=farmer)
self.town.mayor = farmer # TODO actual mayor stuff
# Name the town -- has to come before the cemetery is instantiated,
# so that the cemetery can be named after it
self.town.name = self._generate_name_for_town()
# Establish a cemetery -- it doesn't matter who the owner is for
# public institutions like a cemetery, it will just be used as a
# reference with which to access this simulation instance
Cemetery(owner=self.random_person)
# Set the town's 'settlers' attribute
self.town.settlers = set(self.town.residents)
# Now simulate until the specified date that worldgen ends
print "Simulating {n} years of history...".format(
n=self.config.date_worldgen_ends[0] - self.config.date_worldgen_begins[0]
)
time.sleep(1.2)
n_days_until_worldgen_ends = self.ordinal_date_that_worldgen_ends - self.ordinal_date
n_timesteps_until_worldgen_ends = n_days_until_worldgen_ends * 2
self.simulate(n_timesteps=n_timesteps_until_worldgen_ends)
def _generate_name_for_town(self):
"""Generate a name for the town."""
if random.random() < self.config.chance_town_gets_named_for_a_settler:
name = self.town.mayor.last_name
else:
name = Names.a_place_name()
return name
def assign_event_number(self, new_event):
"""Assign an event number to some event, to allow for precise ordering of events that happened same timestep.
Also add the event to a listing of all simulated events; this facilitates debugging.
"""
self.events.append(new_event)
self.event_number += 1
return self.event_number
@staticmethod
def get_random_day_of_year(year):
"""Return a randomly chosen day in the given year."""
ordinal_date_on_jan_1_of_this_year = datetime.date(year, 1, 1).toordinal()
ordinal_date = (
ordinal_date_on_jan_1_of_this_year + random.randint(0, 365)
)
datetime_object = datetime.date.fromordinal(ordinal_date)
month, day = datetime_object.month, datetime_object.day
return month, day, ordinal_date
def get_date(self, ordinal_date=None):
"""Return a pretty-printed date for ordinal date."""
if not ordinal_date:
ordinal_date = self.ordinal_date
year = datetime.date.fromordinal(ordinal_date).year
month = datetime.date.fromordinal(ordinal_date).month
day = datetime.date.fromordinal(ordinal_date).day
month_ordinals_to_names = {
1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July",
8: "August", 9: "September", 10: "October", 11: "November", 12: "December"
}
date = "{} of {} {}, {}".format(
# Note: for retconning, the time of day will always be whatever the actual time of day
# is at the beginning of the true simulation ("day", I assume), but this shouldn't matter
self.time_of_day.title(), month_ordinals_to_names[month], day, year
)
return date
def simulate(self, n_timesteps=1):
"""Simulate activity in this town for the given number of timesteps."""
for i in xrange(n_timesteps):
# Do some basic bookkeeping, regardless of whether the timestep will be simulated
self.advance_time()
self._progress_town_businesses()
self._simulate_births()
# Potentially simulate the timestep
if random.random() < self.config.chance_of_a_timestep_being_simulated:
self._simulate_timestep()
# Write out samples from the event stream to stdout
try:
recent_event = random.choice(self.events[-10:])
recent_event_str = str(recent_event)[:94]
sys.stdout.write('\r' + recent_event_str.ljust(94))
sys.stdout.flush()
except (NameError, IndexError): # This won't work for the first iteration of the loop
pass
sys.stdout.write('\r{}'.format(' '*94)) # Clear out the last sampled event written to stdout
sys.stdout.write('\rWrapping up...')
def advance_time(self):
"""Advance time of day and date, if it's a new day."""
# Update the time of day
self.time_of_day = "night" if self.time_of_day == "day" else "day"
# If it's a new day, update the date and simulate birthdays
if self.time_of_day == "day":
self._update_date()
self._execute_birthdays()
else:
self.date = self.get_date()
# Set a new random number for this timestep
self.random_number_this_timestep = random.random()
# Lastly, update the weather for today
self.weather = random.choice(['good', 'bad'])
def _update_date(self):
"""Update the current date, given that it's a new day."""
# Increment the ordinate date
self.ordinal_date += 1
# Use that to update the current date
new_date_tuple = datetime.date.fromordinal(self.ordinal_date)
if new_date_tuple.year != self.year:
# Happy New Year
self.true_year = new_date_tuple.year
self.year = new_date_tuple.year
self.month = new_date_tuple.month
self.day = new_date_tuple.day
self.date = self.get_date()
def _execute_birthdays(self):
"""Execute the effects of any birthdays happening today."""
# Age any present (not dead, not departed) character whose birthday is today
if (self.month, self.day) not in self.birthdays:
self.birthdays[(self.month, self.day)] = set()
else:
for person in self.birthdays[(self.month, self.day)]:
if person.present:
person.grow_older()
# Don't forget leap-year babies
if (self.month, self.day) == (3, 1):
for person in self.birthdays[(2, 29)]:
if person.present:
person.grow_older()
def _progress_town_businesses(self):
"""Potentially have new businesses establish and/or existing businesses close down."""
self._potentially_establish_a_new_business()
self._potentially_shut_down_businesses()
def _simulate_births(self):
"""Simulate births, even if this timestep will not actually be simulated."""
for person in list(self.town.residents):
if person.pregnant:
if self.ordinal_date >= person.due_date: # Not worth the computation to be realistic about late births
if self.time_of_day == 'day':
if random.random() < 0.5:
person.give_birth()
else:
person.give_birth()
def _potentially_establish_a_new_business(self):
"""Potentially have a new business get constructed in town."""
config = self.config
# If there's less than 30 vacant homes in this town and no apartment complex
# yet, have one open up
if len(self.town.vacant_lots) < 30 and not self.town.businesses_of_type('ApartmentComplex'):
owner = self._determine_who_will_establish_new_business(business_type=ApartmentComplex)
ApartmentComplex(owner=owner)
elif random.random() < config.chance_a_business_opens_some_timestep:
all_business_types = Business.__subclasses__()
type_of_business_that_will_open = None
tries = 0
while not type_of_business_that_will_open:
tries += 1
randomly_selected_type = random.choice(all_business_types)
advent, demise, min_pop = config.business_types_advent_demise_and_minimum_population[
randomly_selected_type
]
# Check if the business type is era-appropriate
if advent < self.year < demise and self.town.population > min_pop:
# Check if there aren't already too many businesses of this type in town
max_number_for_this_type = config.max_number_of_business_types_at_one_time[randomly_selected_type]
if (len(self.town.businesses_of_type(randomly_selected_type.__name__)) <
max_number_for_this_type):
# Lastly, if this is a business that only forms on a tract, make sure
# there is a vacant tract for it to be established upon
need_tract = randomly_selected_type in config.companies_that_get_established_on_tracts
if (need_tract and self.town.vacant_tracts) or not need_tract:
type_of_business_that_will_open = randomly_selected_type
if self.town.population < 50 or tries > 10: # Just not ready for more businesses yet -- grow naturally
break
if type_of_business_that_will_open in config.public_company_types:
type_of_business_that_will_open(owner=self.town.mayor)
elif type_of_business_that_will_open:
owner = self._determine_who_will_establish_new_business(business_type=type_of_business_that_will_open)
type_of_business_that_will_open(owner=owner)
def _determine_who_will_establish_new_business(self, business_type):
"""Select a person who will establish a new business of the given type."""
config = self.config
occupation_type_for_owner_of_this_type_of_business = (
config.owner_occupations_for_each_business_type[business_type]
)
if occupation_type_for_owner_of_this_type_of_business in config.occupations_requiring_college_degree:
if any(p for p in self.town.residents if p.college_graduate and not p.occupations and
config.employable_as_a[occupation_type_for_owner_of_this_type_of_business](applicant=p)):
# Have a fresh college graduate in town start up a dentist office or whatever it is
owner = next(p for p in self.town.residents if p.college_graduate and not p.occupations and
config.employable_as_a[occupation_type_for_owner_of_this_type_of_business](applicant=p))
else:
# Have someone from outside the town come in
owner = PersonExNihilo(
sim=self, job_opportunity_impetus=occupation_type_for_owner_of_this_type_of_business,
spouse_already_generated=None
)
else:
if config.job_levels[occupation_type_for_owner_of_this_type_of_business] < 3:
# Have a young person step up and start their career as a tradesman
if any(p for p in self.town.residents if p.in_the_workforce and not p.occupations and
config.employable_as_a[occupation_type_for_owner_of_this_type_of_business](applicant=p)):
owner = next(
p for p in self.town.residents if p.in_the_workforce and not p.occupations and
config.employable_as_a[occupation_type_for_owner_of_this_type_of_business](applicant=p)
)
# Have any unemployed person in town try their hand at running a business
elif any(p for p in self.town.residents if not p.retired and not p.occupation and p.in_the_workforce and
config.employable_as_a[occupation_type_for_owner_of_this_type_of_business](applicant=p)):
owner = next(
p for p in self.town.residents if not p.retired and not p.occupation and p.in_the_workforce and
config.employable_as_a[occupation_type_for_owner_of_this_type_of_business](applicant=p)
)
else:
# Have someone from outside the town come in
owner = PersonExNihilo(
sim=self, job_opportunity_impetus=occupation_type_for_owner_of_this_type_of_business,
spouse_already_generated=None
)
else:
# Have someone from outside the town come in
owner = PersonExNihilo(
sim=self, job_opportunity_impetus=occupation_type_for_owner_of_this_type_of_business,
spouse_already_generated=None
)
return owner
def _potentially_shut_down_businesses(self):
"""Potentially have a new business get constructed in town."""
config = self.config
chance_a_business_shuts_down_this_timestep = config.chance_a_business_closes_some_timestep
chance_a_business_shuts_down_on_timestep_after_its_demise = (
# Once its anachronistic, like a Dairy in 1960
config.chance_a_business_shuts_down_on_timestep_after_its_demise
)
for business in list(self.town.companies):
if business.demise <= self.year:
if random.random() < chance_a_business_shuts_down_on_timestep_after_its_demise:
if business.__class__ not in config.public_company_types:
business.go_out_of_business(reason=None)
elif random.random() < chance_a_business_shuts_down_this_timestep:
if business.__class__ not in config.public_company_types:
if not (
# Don't shut down an apartment complex with people living in it,
# or an apartment complex that's the only one in town
business.__class__ is ApartmentComplex and business.residents or
len(self.town.businesses_of_type('ApartmentComplex')) == 1
):
business.go_out_of_business(reason=None)
def _simulate_timestep(self):
"""Simulate town activity for a single timestep."""
self.n_simulated_timesteps += 1
for person in list(self.town.residents):
self._simulate_life_events_for_a_person_on_this_timestep(person=person)
days_since_last_simulated_day = self.ordinal_date - self.last_simulated_day
# Reset all Relationship interacted_this_timestep attributes
for person in list(self.town.residents):
for other_person in person.relationships:
person.relationships[other_person].interacted_this_timestep = False
# Have people go to the location they will be at this timestep
for person in list(self.town.residents):
person.routine.enact()
# Have people initiate social interactions with one another
for person in list(self.town.residents):
# Person may have married (during an earlier iteration of this loop) and
# then immediately departed because the new couple could not find home,
# so we still have to make sure they actually live in the town currently before
# having them socialize
if person in self.town.residents:
if person.age > 3: # Must be at least four years old to socialize
person.socialize(missing_timesteps_to_account_for=days_since_last_simulated_day * 2)
self.last_simulated_day = self.ordinal_date
def _simulate_life_events_for_a_person_on_this_timestep(self, person):
"""Simulate the life of the given person on this timestep."""
# First, we need to make sure that this person didn't already die or leave town
# on an earlier iteration of this loop (e.g., a child whose parents decided to move)
if person.present:
self._simulate_prospect_of_death(person=person)
# Throughout this block, we have to keep checking that the person is still present
# because one of the steps in this procedure may cause them to either die or
# leave town, and upon either of those occurring we stop simulating their life
if person.present and not person.spouse:
self._simulate_dating(person=person)
if person.present and person.spouse:
self._simulate_marriage(person=person)
if person.present and person.occupation:
self._simulate_prospect_of_retirement(person=person)
elif person.present and person.in_the_workforce and not person.occupation and not person.retired:
self._simulate_unemployment(person=person)
elif person.present and person not in person.home.owners:
self._simulate_moving_out_of_parents(person=person)
def _simulate_prospect_of_death(self, person):
"""Simulate the potential for this person to die on this timestep."""
if person.age > 68 and random.random() > self.config.chance_someone_dies_some_timestep:
person.die(cause_of_death="Natural causes")
def _simulate_dating(self, person):
"""Simulate the dating life of this person."""
# I haven't yet implemented any systems modeling/simulating characters dating
# or leading romantic lives at all, really; I'd love to do this at some point,
# but currently all that is happening is characters evolve their romantic
# affinities for one another (as a function of nonreciprocal romantic affinities
# and amount of time spent together; see relationship.py), and if a threshold
# for mutual romantic affinity is eclipsed, they may marry (right on this timestep)
if person.age >= self.config.marriageable_age:
min_mutual_spark_for_proposal = self.config.min_mutual_spark_value_for_someone_to_propose_marriage
people_they_have_strong_romantic_feelings_for = [
p for p in person.relationships if person.relationships[p].spark > min_mutual_spark_for_proposal
]
for prospective_partner in people_they_have_strong_romantic_feelings_for:
if prospective_partner.age >= self.config.marriageable_age:
if prospective_partner.present and not prospective_partner.spouse:
if prospective_partner.relationships[person].spark > min_mutual_spark_for_proposal:
person.marry(partner=prospective_partner)
break
def _simulate_marriage(self, person):
"""Simulate basic marriage activities for this person."""
self._simulate_prospect_of_conception(person=person)
self._simulate_prospect_of_divorce(person=person)
def _simulate_prospect_of_conception(self, person):
"""Simulate the potential for conception today in the course of this person's marriage."""
chance_they_are_trying_to_conceive_this_year = (
self.config.function_to_determine_chance_married_couple_are_trying_to_conceive(
n_kids=len(person.marriage.children_produced)
)
)
chance_they_are_trying_to_conceive_this_timestep = (
# Don't need 720, which is actual number of timesteps in a year, because their spouse will also
# be iterated over on this timestep
chance_they_are_trying_to_conceive_this_year / (self.config.chance_of_a_timestep_being_simulated * 365)
)
if random.random() < chance_they_are_trying_to_conceive_this_timestep:
person.have_sex(partner=person.spouse, protection=False)
# Note: sex doesn't happen otherwise because no interesting phenomena surrounding it are
# modeled/simulated; it's currently just a mechanism for bringing new characters into the world
def _simulate_prospect_of_divorce(self, person):
"""Simulate the potential for divorce today in the course of this person's marriage."""
# Check if this person is significantly more in love with someone else in town
if person.love_interest:
if person.love_interest is not person.spouse and person.love_interest.present:
if random.random() < self.config.chance_a_divorce_happens_some_timestep:
person.divorce(partner=person.spouse)
def _simulate_prospect_of_retirement(self, person):
"""Simulate the potential for this person to retire on this timestep."""
if person.occupation and person.age > max(65, random.random() * 100):
person.retire()
def _simulate_unemployment(self, person):
"""Simulate the given person searching for work, which may involve them getting a
college education or deciding to leave town.
"""
person.look_for_work()
if not person.occupation: # Means look_for_work() didn't succeed
if (not person.college_graduate and person.age > 22 and
person.male if self.year < 1920 else True):
person.college_graduate = True
elif random.random() < self.config.chance_an_unemployed_person_departs_on_a_simulated_timestep:
if not (person.spouse and person.spouse.occupation):
person.depart_town()
def _simulate_moving_out_of_parents(self, person):
"""Simulate the potential for this person to move out of their parents' home."""
if person.occupation:
if random.random() < self.config.chance_employed_adult_will_move_out_of_parents_on_simulated_timestep:
person.move_out_of_parents()
def find(self, name):
"""Return person living in this town with that name."""
if any(p for p in self.town.residents if p.name == name):
people_named_this = [p for p in self.town.residents if p.name == name]
if len(people_named_this) > 1:
print '\nWarning: Multiple {} residents are named {}; returning a complete list\n'.format(
self.town.name, name
)
return people_named_this
else:
return people_named_this[0]
else:
raise Exception('There is no one in {} named {}'.format(self.town.name, name))
def find_deceased(self, name):
"""Return deceased person with that name."""
if any(p for p in self.town.deceased if p.name == name):
people_named_this = [p for p in self.town.deceased if p.name == name]
if len(people_named_this) > 1:
print '\nWarning: Multiple {} residents are named {}; returning a complete list\n'.format(
self.town.name, name
)
return people_named_this
else:
return people_named_this[0]
else:
raise Exception('There is no one named {} who died in {]'.format(name, self.town.name))
def find_by_hex(self, hex_value):
"""Return person whose ID in memory has the given hex value."""
int_of_hex = int(hex_value, 16)
try:
person = next(
p for p in self.town.residents | self.town.deceased | self.town.departed if
id(p) == int_of_hex
)
return person
except StopIteration:
raise Exception('There is no one with that hex ID')
def find_co(self, name):
"""Return company in this town with the given name."""
try:
company = next(c for c in self.town.companies if c.name == name)
return company
except StopIteration:
raise Exception('There is no company in {} named {}'.format(self.town.name, name))