pykworldsim 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ from .world import World
2
+ from .person import Person
3
+ from .relationship import Relationship
4
+ from .goal import Goal
5
+ from .event import Event
6
+ from .location import Location
7
+ from .job import Job
8
+
9
+ __all__ = ["World", "Person", "Relationship", "Goal", "Event", "Location", "Job"]
10
+ __version__ = "1.0.0"
pykworldsim/event.py ADDED
@@ -0,0 +1,28 @@
1
+ import random
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Callable
4
+
5
+
6
+ @dataclass
7
+ class Event:
8
+ """A world event that fires probabilistically or on demand."""
9
+ Name: str
10
+ Probability: float = 0.05 # per-year probability
11
+ Effect: Optional[Callable] = None # fn(world, year) → None
12
+ Description: str = ""
13
+ IsGlobal: bool = True # True = affects everyone; False = individual
14
+
15
+ def ShouldTrigger(self) -> bool:
16
+ return random.random() < self.Probability
17
+
18
+ def Fire(self, World, Year: int):
19
+ Msg = f"[Year {Year}] EVENT: {self.Name}"
20
+ if self.Description:
21
+ Msg += f" — {self.Description}"
22
+ World.Log.append(Msg)
23
+ if self.Effect:
24
+ self.Effect(World, Year)
25
+
26
+ def __str__(self):
27
+ Scope = "Global" if self.IsGlobal else "Local"
28
+ return f"Event: {self.Name} ({self.Probability*100:.1f}%) - {Scope}"
pykworldsim/goal.py ADDED
@@ -0,0 +1,34 @@
1
+ class Goal:
2
+ """Represents a personal goal with a type and priority."""
3
+
4
+ ValidGoalTypes = {
5
+ "GetJob", "GetPromotion", "MakeFriends", "FindPartner",
6
+ "ImproveSkills", "EarnMoney", "IncreaseStatus", "MoveCity",
7
+ "StartFamily", "BuildBusiness", "SocializeMore", "FindPurpose"
8
+ }
9
+
10
+ def __init__(self, GoalType: str, Priority: float = 0.5):
11
+ if GoalType not in self.ValidGoalTypes:
12
+ raise ValueError(f"GoalType '{GoalType}' is not valid. Choose from: {sorted(self.ValidGoalTypes)}")
13
+ if not (0.0 <= Priority <= 1.0):
14
+ raise ValueError("Priority must be between 0.0 and 1.0")
15
+ self.GoalType = GoalType
16
+ self.Priority = Priority
17
+ self.Progress = 0.0
18
+ self.Achieved = False
19
+ self.YearAchieved = None
20
+
21
+ def UpdateProgress(self, Delta: float):
22
+ self.Progress = max(0.0, min(1.0, self.Progress + Delta))
23
+ if self.Progress >= 1.0 and not self.Achieved:
24
+ self.Achieved = True
25
+ return True
26
+ return False
27
+
28
+ def __repr__(self):
29
+ Status = "✓" if self.Achieved else f"{self.Progress:.0%}"
30
+ return f"Goal({self.GoalType}, priority={self.Priority:.1f}, progress={Status})"
31
+
32
+ def __str__(self):
33
+ Status = "Achieved" if self.Achieved else f"{self.Progress:.0%} Complete"
34
+ return f"Goal: {self.GoalType} ({Status})"
pykworldsim/job.py ADDED
@@ -0,0 +1,34 @@
1
+ class Job:
2
+ """A job that can be held by a person."""
3
+
4
+ def __init__(self, Role: str, Salary: float = 50000,
5
+ Prestige: float = 0.5, StressLevel: float = 0.4,
6
+ SkillRequired: float = 0.3, Industry: str = "general"):
7
+ self.Role = Role
8
+ self.Salary = Salary
9
+ self.Prestige = max(0.0, min(1.0, Prestige))
10
+ self.StressLevel = max(0.0, min(1.0, StressLevel))
11
+ self.SkillRequired = max(0.0, min(1.0, SkillRequired))
12
+ self.Industry = Industry
13
+ self.Holder = None
14
+ self.Location = None
15
+
16
+ def IsAvailable(self) -> bool:
17
+ return self.Holder is None
18
+
19
+ def Assign(self, Person):
20
+ self.Holder = Person
21
+
22
+ def Vacate(self):
23
+ self.Holder = None
24
+
25
+ def AnnualSalary(self, Year: int = 0) -> float:
26
+ """Salary grows slightly with experience."""
27
+ return self.Salary * (1.02 ** Year)
28
+
29
+ def __repr__(self):
30
+ Status = f"held by {self.Holder.Name}" if self.Holder else "available"
31
+ return f"Job({self.Role}, salary={self.Salary:,.0f}, {Status})"
32
+
33
+ def __str__(self):
34
+ return f"{self.Role} (${self.Salary:,.0f}/yr) - {'Available' if not self.Holder else f'Held by {self.Holder.Name}'}"
@@ -0,0 +1,31 @@
1
+ class Location:
2
+ """A place in the world where people live, work, and interact."""
3
+
4
+ def __init__(self, Name: str, LocationType: str = "city",
5
+ OpportunityLevel: float = 0.5,
6
+ CostOfLiving: float = 0.5,
7
+ PopulationDensity: float = 0.5):
8
+ self.Name = Name
9
+ self.LocationType = LocationType # city, suburb, rural, campus
10
+ self.OpportunityLevel = max(0.0, min(1.0, OpportunityLevel))
11
+ self.CostOfLiving = max(0.0, min(1.0, CostOfLiving))
12
+ self.PopulationDensity = max(0.0, min(1.0, PopulationDensity))
13
+ self.Residents: list = []
14
+ self.Jobs: list = []
15
+
16
+ def AddResident(self, Person):
17
+ if Person not in self.Residents:
18
+ self.Residents.append(Person)
19
+
20
+ def RemoveResident(self, Person):
21
+ if Person in self.Residents:
22
+ self.Residents.remove(Person)
23
+
24
+ def AddJob(self, Job):
25
+ self.Jobs.append(Job)
26
+
27
+ def __repr__(self):
28
+ return f"Location({self.Name}, type={self.LocationType}, residents={len(self.Residents)})"
29
+
30
+ def __str__(self):
31
+ return f"{self.Name} ({self.LocationType.title()}) - Population: {len(self.Residents)}"
pykworldsim/person.py ADDED
@@ -0,0 +1,468 @@
1
+ import random
2
+ import math
3
+ from typing import Dict, List, Optional, Any
4
+ from .goal import Goal
5
+ from .relationship import Relationship
6
+
7
+
8
+ class Person:
9
+ """
10
+ A simulated person with traits, goals, internal state, and social connections.
11
+ All variable names use PascalCase.
12
+ """
13
+
14
+ _IdCounter = 0
15
+
16
+ # Default trait distributions if not specified
17
+ _DefaultTraitRanges: Dict[str, tuple] = {
18
+ "Openness": (0.2, 0.9),
19
+ "Conscientiousness": (0.2, 0.9),
20
+ "Extraversion": (0.1, 0.9),
21
+ "Agreeableness": (0.2, 0.9),
22
+ "Neuroticism": (0.1, 0.8),
23
+ "Ambition": (0.1, 0.9),
24
+ "Intelligence": (0.2, 0.9),
25
+ "SocialSkill": (0.2, 0.9),
26
+ "Creativity": (0.1, 0.9),
27
+ "RiskTolerance": (0.1, 0.9),
28
+ "Discipline": (0.1, 0.9),
29
+ }
30
+
31
+ def __init__(self, Name: str, Age: int = 20,
32
+ Traits: Optional[Dict[str, float]] = None,
33
+ Location: Optional[Any] = None):
34
+ Person._IdCounter += 1
35
+ self.Id = Person._IdCounter
36
+ self.Name = Name
37
+ self.Age = Age
38
+ self.Location = Location
39
+ self.IsAlive = True
40
+
41
+ # --- Traits (static-ish personality) ---
42
+ self.Traits: Dict[str, float] = {}
43
+ for TraitName, (Lo, Hi) in self._DefaultTraitRanges.items():
44
+ self.Traits[TraitName] = round(random.uniform(Lo, Hi), 3)
45
+ if Traits:
46
+ for K, V in Traits.items():
47
+ NormKey = self._NormalizeTraitKey(K)
48
+ self.Traits[NormKey] = max(0.0, min(1.0, float(V)))
49
+
50
+ # --- Internal state (dynamic) ---
51
+ self.Mood = random.uniform(0.4, 0.7)
52
+ self.Energy = random.uniform(0.5, 0.9)
53
+ self.Stress = random.uniform(0.1, 0.4)
54
+ self.Motivation = random.uniform(0.4, 0.8)
55
+ self.LifeSatisfaction = random.uniform(0.3, 0.7)
56
+
57
+ # --- Life metrics ---
58
+ self.Income = 0.0
59
+ self.Savings = 0.0
60
+ self.Status = random.uniform(0.1, 0.4)
61
+ self.Education = random.uniform(0.1, 0.4)
62
+ self.SkillLevel = random.uniform(0.1, 0.4)
63
+ self.HasDegree = False
64
+
65
+ # --- Social ---
66
+ self.Relationships: Dict[int, Relationship] = {} # Other person's Id → Relationship
67
+ self.RomanticPartner: Optional["Person"] = None
68
+ self.Children: List["Person"] = []
69
+ self.Parents: List["Person"] = []
70
+
71
+ # --- Goals ---
72
+ self.Goals: List[Goal] = []
73
+ self._GenerateInitialGoals()
74
+
75
+ # --- Career ---
76
+ self.Job: Optional[Any] = None
77
+ self.YearsAtCurrentJob: int = 0
78
+ self.CareerHistory: List[str] = []
79
+
80
+ # --- Life log ---
81
+ self.LifeLog: List[str] = []
82
+
83
+ # --- Tracking ---
84
+ self.HappinessHistory: List[float] = []
85
+ self.IncomeHistory: List[float] = []
86
+ self.YearBorn: int = 0 # Set by World
87
+
88
+ # ------------------------------------------------------------------
89
+ # Trait normalization
90
+ # ------------------------------------------------------------------
91
+ def _NormalizeTraitKey(self, Key: str) -> str:
92
+ """Normalise user-supplied trait keys to PascalCase canonical names."""
93
+ Mapping = {
94
+ "openness": "Openness",
95
+ "conscientiousness": "Conscientiousness",
96
+ "extraversion": "Extraversion",
97
+ "agreeableness": "Agreeableness",
98
+ "neuroticism": "Neuroticism",
99
+ "ambition": "Ambition",
100
+ "intelligence": "Intelligence",
101
+ "socialskill": "SocialSkill",
102
+ "social": "SocialSkill",
103
+ "creativity": "Creativity",
104
+ "risktolerance": "RiskTolerance",
105
+ "risk": "RiskTolerance",
106
+ "discipline": "Discipline",
107
+ }
108
+ return Mapping.get(Key.lower().replace("_", ""), Key)
109
+
110
+ # ------------------------------------------------------------------
111
+ # Goals
112
+ # ------------------------------------------------------------------
113
+ def _GenerateInitialGoals(self):
114
+ Ambition = self.Traits.get("Ambition", 0.5)
115
+ Social = self.Traits.get("SocialSkill", 0.5)
116
+ if Ambition > 0.6:
117
+ self.Goals.append(Goal("GetJob", Priority=0.9))
118
+ self.Goals.append(Goal("GetPromotion", Priority=0.7))
119
+ self.Goals.append(Goal("EarnMoney", Priority=0.7))
120
+ else:
121
+ self.Goals.append(Goal("GetJob", Priority=0.6))
122
+ if Social > 0.5:
123
+ self.Goals.append(Goal("MakeFriends", Priority=0.7))
124
+ if self.Traits.get("Openness", 0.5) > 0.6:
125
+ self.Goals.append(Goal("ImproveSkills", Priority=0.5))
126
+ self.Goals.append(Goal("FindPartner", Priority=round(random.uniform(0.3, 0.8), 2)))
127
+
128
+ def AddGoal(self, GoalType: str, Priority: float = 0.5):
129
+ self.Goals.append(Goal(GoalType, Priority))
130
+
131
+ # ------------------------------------------------------------------
132
+ # Core simulation tick
133
+ # ------------------------------------------------------------------
134
+ def Tick(self, Year: int, World: Any):
135
+ if not self.IsAlive:
136
+ return
137
+ self.Age += 1
138
+ self._UpdateInternalState()
139
+ self._MakeDecisions(Year, World)
140
+ self._UpdateGoals(Year)
141
+ self._CheckMortality(Year, World)
142
+ self.HappinessHistory.append(round(self.LifeSatisfaction, 3))
143
+ self.IncomeHistory.append(round(self.Income, 2))
144
+
145
+ # ------------------------------------------------------------------
146
+ # Internal state update
147
+ # ------------------------------------------------------------------
148
+ def _UpdateInternalState(self):
149
+ Neuroticism = self.Traits.get("Neuroticism", 0.4)
150
+ Extraversion = self.Traits.get("Extraversion", 0.5)
151
+ FriendCount = self._CountRelationshipsByType(["friend", "close_friend", "partner"])
152
+
153
+ # Mood fluctuates; neuroticism increases variance
154
+ MoodShift = random.gauss(0, 0.08 * (0.5 + Neuroticism))
155
+ self.Mood = max(0.0, min(1.0, self.Mood + MoodShift))
156
+
157
+ # Stress from job and lack of money
158
+ JobStress = self.Job.StressLevel if self.Job else 0.1
159
+ FinancialStress = max(0.0, 0.5 - min(1.0, self.Savings / 20000)) * 0.3
160
+ self.Stress = max(0.0, min(1.0, JobStress * 0.6 + FinancialStress + random.uniform(-0.05, 0.05)))
161
+
162
+ # Energy: discipline and sleep patterns
163
+ Discipline = self.Traits.get("Discipline", 0.5)
164
+ self.Energy = max(0.1, min(1.0, Discipline * 0.5 + random.uniform(0.1, 0.4)))
165
+
166
+ # Motivation: goal progress and mood
167
+ ActiveGoals = [G for G in self.Goals if not G.Achieved]
168
+ GoalFactor = 0.5 if not ActiveGoals else min(1.0, sum(G.Priority for G in ActiveGoals) / len(ActiveGoals))
169
+ self.Motivation = max(0.1, min(1.0, GoalFactor * 0.6 + self.Mood * 0.4))
170
+
171
+ # Life satisfaction: multi-factor
172
+ SocialFactor = min(1.0, FriendCount / 5.0) * Extraversion
173
+ CareerFactor = (self.Job.Prestige if self.Job else 0.0) * self.Traits.get("Ambition", 0.5)
174
+ FinanceFactor = min(1.0, self.Savings / 50000) * 0.3
175
+ PartnerFactor = 0.15 if self.RomanticPartner else 0.0
176
+ BaseSatisfaction = (SocialFactor * 0.3 + CareerFactor * 0.25 +
177
+ FinanceFactor + self.Mood * 0.2 + PartnerFactor)
178
+ self.LifeSatisfaction = max(0.0, min(1.0,
179
+ self.LifeSatisfaction * 0.7 + BaseSatisfaction * 0.3))
180
+
181
+ # Savings accumulate from income minus cost of living
182
+ if self.Income > 0:
183
+ LivingCost = (self.Location.CostOfLiving * 30000 if self.Location else 20000)
184
+ AnnualSavings = max(0, self.Income - LivingCost) * random.uniform(0.2, 0.6)
185
+ self.Savings += AnnualSavings
186
+
187
+ # ------------------------------------------------------------------
188
+ # Decision engine
189
+ # ------------------------------------------------------------------
190
+ def _MakeDecisions(self, Year: int, World: Any):
191
+ # Prioritise highest-priority unachieved goal
192
+ ActiveGoals = sorted([G for G in self.Goals if not G.Achieved],
193
+ key=lambda G: G.Priority, reverse=True)
194
+ if not ActiveGoals:
195
+ return
196
+
197
+ TopGoal = ActiveGoals[0]
198
+
199
+ if TopGoal.GoalType == "GetJob" and self.Job is None:
200
+ self._SeekJob(Year, World)
201
+ elif TopGoal.GoalType == "GetPromotion" and self.Job is not None:
202
+ self._SeekPromotion(Year, World)
203
+ elif TopGoal.GoalType == "MakeFriends":
204
+ self._Socialize(Year, World)
205
+ elif TopGoal.GoalType == "FindPartner" and self.RomanticPartner is None:
206
+ self._SeekPartner(Year, World)
207
+ elif TopGoal.GoalType == "EarnMoney":
208
+ self._OptimiseCareer(Year, World)
209
+ elif TopGoal.GoalType == "ImproveSkills":
210
+ self._ImproveSkills(Year, World)
211
+ elif TopGoal.GoalType == "MoveCity":
212
+ self._ConsiderMoving(Year, World)
213
+
214
+ # Always do a little socialising
215
+ if random.random() < self.Traits.get("Extraversion", 0.5):
216
+ self._Socialize(Year, World, Intensity="light")
217
+
218
+ def _SeekJob(self, Year: int, World: Any):
219
+ AvailableJobs = [J for J in World.Jobs if J.IsAvailable() and
220
+ J.SkillRequired <= self.SkillLevel + 0.2 and
221
+ (J.Location is None or self.Location is None or J.Location == self.Location)]
222
+ if not AvailableJobs:
223
+ return
224
+ # Pick best paying job within skill reach
225
+ BestJob = max(AvailableJobs, key=lambda J: J.Salary * (1 + self.Traits.get("Ambition", 0.5)))
226
+ Chance = 0.3 + self.SkillLevel * 0.4 + self.Traits.get("Intelligence", 0.5) * 0.2
227
+ if random.random() < Chance:
228
+ BestJob.Assign(self)
229
+ self.Job = BestJob
230
+ self.Income = BestJob.Salary
231
+ self.YearsAtCurrentJob = 0
232
+ self.CareerHistory.append(f"Year {Year}: Got job as {BestJob.Role} (${BestJob.Salary:,.0f})")
233
+ self.LifeLog.append(f"Year {Year}: Started working as a {BestJob.Role}")
234
+ World.Log.append(f"{self.Name} got a job as {BestJob.Role}")
235
+
236
+ def _SeekPromotion(self, Year: int, World: Any):
237
+ if not self.Job or self.YearsAtCurrentJob < 2:
238
+ return
239
+ PromotionChance = (self.Traits.get("Ambition", 0.5) * 0.3 +
240
+ self.SkillLevel * 0.3 +
241
+ self.Traits.get("Conscientiousness", 0.5) * 0.2 +
242
+ random.uniform(0, 0.2))
243
+ if random.random() < PromotionChance * 0.4:
244
+ Raise = self.Income * random.uniform(0.10, 0.25)
245
+ self.Income += Raise
246
+ self.Job.Salary += Raise
247
+ self.Status = min(1.0, self.Status + 0.05)
248
+ self.LifeLog.append(f"Year {Year}: Got promoted! Now earning ${self.Income:,.0f}")
249
+ World.Log.append(f"{self.Name} received a promotion (now ${self.Income:,.0f}/yr)")
250
+ self.YearsAtCurrentJob += 1
251
+
252
+ def _Socialize(self, Year: int, World: Any, Intensity: str = "normal"):
253
+ Candidates = [P for P in World.People
254
+ if P.Id != self.Id and P.IsAlive and
255
+ (P.Location == self.Location or random.random() < 0.1)]
256
+ if not Candidates:
257
+ return
258
+ NumMeet = 1 if Intensity == "light" else random.randint(1, 3)
259
+ for _ in range(NumMeet):
260
+ if not Candidates:
261
+ break
262
+ Other = random.choice(Candidates)
263
+ Candidates.remove(Other)
264
+ Context = random.choice(["casual", "party", "work", "support"])
265
+ self._InteractWith(Other, Context, Year, World)
266
+
267
+ def _InteractWith(self, Other: "Person", Context: str, Year: int, World: Any):
268
+ # Get or create relationship
269
+ if Other.Id not in self.Relationships:
270
+ Rel = Relationship(self, Other)
271
+ Rel.YearMet = Year
272
+ Rel.RecordEvent(Year, "met", f"{self.Name} met {Other.Name}")
273
+ self.Relationships[Other.Id] = Rel
274
+ Other.Relationships[self.Id] = Rel
275
+ World.Log.append(f"{self.Name} met {Other.Name}")
276
+
277
+ Rel = self.Relationships[Other.Id]
278
+ Rel.Interact(Context=Context, Year=Year)
279
+
280
+ # Romantic spark: if both single and good chemistry
281
+ if (self.RomanticPartner is None and Other.RomanticPartner is None and
282
+ Rel.Strength > 0.5 and Rel.Trust > 0.4 and
283
+ random.random() < 0.08 * self.Traits.get("Extraversion", 0.5)):
284
+ self._StartRomance(Other, Year, World)
285
+
286
+ def _StartRomance(self, Other: "Person", Year: int, World: Any):
287
+ self.RomanticPartner = Other
288
+ Other.RomanticPartner = self
289
+ Rel = self.Relationships[Other.Id]
290
+ Rel.IsRomantic = True
291
+ Rel.Attraction = min(1.0, Rel.Attraction + random.uniform(0.3, 0.5))
292
+ Rel.UpdateType()
293
+ self.LifeLog.append(f"Year {Year}: Started a romantic relationship with {Other.Name}")
294
+ World.Log.append(f"{self.Name} and {Other.Name} started a romantic relationship ❤️")
295
+
296
+ # Maybe start a family
297
+ if (random.random() < 0.25 and self.Age < 40 and Other.Age < 40 and
298
+ self.Savings > 5000 and Other.Savings > 5000):
299
+ self._HaveChild(Other, Year, World)
300
+
301
+ def _HaveChild(self, Partner: "Person", Year: int, World: Any):
302
+ ChildName = f"Child_of_{self.Name}"
303
+ # Inherit blended traits
304
+ BlendedTraits = {}
305
+ for Trait in self.Traits:
306
+ MyVal = self.Traits.get(Trait, 0.5)
307
+ PartnerVal = Partner.Traits.get(Trait, 0.5)
308
+ # Weighted blend with small mutation
309
+ Inherited = (MyVal + PartnerVal) / 2 + random.gauss(0, 0.08)
310
+ BlendedTraits[Trait] = max(0.0, min(1.0, Inherited))
311
+
312
+ Child = Person(Name=ChildName, Age=0, Traits=BlendedTraits, Location=self.Location)
313
+ Child.YearBorn = Year
314
+ Child.Parents = [self, Partner]
315
+ self.Children.append(Child)
316
+ Partner.Children.append(Child)
317
+ World.AddPerson(Child)
318
+ self.LifeLog.append(f"Year {Year}: Had a child with {Partner.Name}")
319
+ World.Log.append(f"{self.Name} and {Partner.Name} welcomed a child 👶")
320
+
321
+ def _SeekPartner(self, Year: int, World: Any):
322
+ # Delegate to socialise — romance emerges from interactions
323
+ self._Socialize(Year, World)
324
+
325
+ def _OptimiseCareer(self, Year: int, World: Any):
326
+ if self.Job is None:
327
+ self._SeekJob(Year, World)
328
+ else:
329
+ # Look for better-paying job
330
+ BetterJobs = [J for J in World.Jobs if J.IsAvailable() and
331
+ J.Salary > self.Income * 1.15 and
332
+ J.SkillRequired <= self.SkillLevel + 0.1]
333
+ if BetterJobs and random.random() < 0.3 * self.Traits.get("RiskTolerance", 0.5):
334
+ self.Job.Vacate()
335
+ NewJob = max(BetterJobs, key=lambda J: J.Salary)
336
+ NewJob.Assign(self)
337
+ self.Job = NewJob
338
+ self.Income = NewJob.Salary
339
+ self.YearsAtCurrentJob = 0
340
+ World.Log.append(f"{self.Name} switched to a better-paying job as {NewJob.Role}")
341
+
342
+ def _ImproveSkills(self, Year: int, World: Any):
343
+ GainRate = self.Traits.get("Openness", 0.5) * 0.05 + self.Traits.get("Discipline", 0.5) * 0.05
344
+ self.SkillLevel = min(1.0, self.SkillLevel + GainRate + random.uniform(0, 0.03))
345
+ if not self.HasDegree and self.Age < 28 and random.random() < 0.2:
346
+ self.HasDegree = True
347
+ self.Education = min(1.0, self.Education + 0.3)
348
+ self.LifeLog.append(f"Year {Year}: Earned a university degree")
349
+ World.Log.append(f"{self.Name} earned a degree 🎓")
350
+
351
+ def _ConsiderMoving(self, Year: int, World: Any):
352
+ if not World.Locations or random.random() > self.Traits.get("RiskTolerance", 0.5) * 0.3:
353
+ return
354
+ BetterLocations = [L for L in World.Locations
355
+ if L != self.Location and L.OpportunityLevel > (self.Location.OpportunityLevel if self.Location else 0.5)]
356
+ if BetterLocations:
357
+ NewLoc = random.choice(BetterLocations)
358
+ if self.Location:
359
+ self.Location.RemoveResident(self)
360
+ self.Location = NewLoc
361
+ NewLoc.AddResident(self)
362
+ self.LifeLog.append(f"Year {Year}: Moved to {NewLoc.Name}")
363
+ World.Log.append(f"{self.Name} moved to {NewLoc.Name}")
364
+
365
+ # ------------------------------------------------------------------
366
+ # Goal updates
367
+ # ------------------------------------------------------------------
368
+ def _UpdateGoals(self, Year: int):
369
+ for Goal in self.Goals:
370
+ if Goal.Achieved:
371
+ continue
372
+ if Goal.GoalType == "GetJob":
373
+ Delta = 0.5 if self.Job else random.uniform(-0.02, 0.05)
374
+ elif Goal.GoalType == "GetPromotion":
375
+ Delta = 0.1 if (self.Job and self.YearsAtCurrentJob >= 2) else 0.0
376
+ elif Goal.GoalType == "MakeFriends":
377
+ FriendCount = self._CountRelationshipsByType(["friend", "close_friend"])
378
+ Delta = min(0.1, FriendCount * 0.05)
379
+ elif Goal.GoalType == "FindPartner":
380
+ Delta = 0.5 if self.RomanticPartner else 0.0
381
+ elif Goal.GoalType == "EarnMoney":
382
+ Delta = min(0.1, self.Income / 200000)
383
+ elif Goal.GoalType == "ImproveSkills":
384
+ Delta = self.SkillLevel * 0.1
385
+ elif Goal.GoalType == "StartFamily":
386
+ Delta = 0.5 if self.Children else 0.0
387
+ else:
388
+ Delta = random.uniform(0, 0.03)
389
+
390
+ Achieved = Goal.UpdateProgress(Delta)
391
+ if Achieved:
392
+ Goal.YearAchieved = Year
393
+ self.LifeLog.append(f"Year {Year}: Achieved goal — {Goal.GoalType}!")
394
+ self.LifeSatisfaction = min(1.0, self.LifeSatisfaction + 0.1)
395
+ # Create a new stretch goal
396
+ self._AddStretchGoal(Goal.GoalType)
397
+
398
+ def _AddStretchGoal(self, AchievedGoalType: str):
399
+ StretchMap = {
400
+ "GetJob": "GetPromotion",
401
+ "GetPromotion": "EarnMoney",
402
+ "MakeFriends": "SocializeMore",
403
+ "FindPartner": "StartFamily",
404
+ "ImproveSkills": "BuildBusiness",
405
+ "EarnMoney": "IncreaseStatus",
406
+ }
407
+ NextGoal = StretchMap.get(AchievedGoalType)
408
+ if NextGoal and not any(G.GoalType == NextGoal for G in self.Goals):
409
+ self.Goals.append(Goal(NextGoal, Priority=0.7))
410
+
411
+ # ------------------------------------------------------------------
412
+ # Mortality
413
+ # ------------------------------------------------------------------
414
+ def _CheckMortality(self, Year: int, World: Any):
415
+ if self.Age < 40:
416
+ BaseDeathChance = 0.002
417
+ elif self.Age < 60:
418
+ BaseDeathChance = 0.005
419
+ elif self.Age < 80:
420
+ BaseDeathChance = 0.02
421
+ else:
422
+ BaseDeathChance = 0.08
423
+
424
+ StressModifier = self.Stress * 0.005
425
+ if random.random() < BaseDeathChance + StressModifier:
426
+ self.IsAlive = False
427
+ self.LifeLog.append(f"Year {Year}: Died at age {self.Age}")
428
+ World.Log.append(f"💀 {self.Name} passed away at age {self.Age}")
429
+ if self.Job:
430
+ self.Job.Vacate()
431
+ self.Job = None
432
+ if self.RomanticPartner:
433
+ self.RomanticPartner.RomanticPartner = None
434
+
435
+ # ------------------------------------------------------------------
436
+ # Helpers
437
+ # ------------------------------------------------------------------
438
+ def _CountRelationshipsByType(self, Types: list) -> int:
439
+ return sum(1 for Rel in self.Relationships.values() if Rel.RelationshipType in Types)
440
+
441
+ def GetRelationship(self, Other: "Person") -> "Relationship | None":
442
+ return self.Relationships.get(Other.Id)
443
+
444
+ def Summary(self) -> dict:
445
+ return {
446
+ "Name": self.Name,
447
+ "Age": self.Age,
448
+ "Alive": self.IsAlive,
449
+ "Job": self.Job.Role if self.Job else "Unemployed",
450
+ "Income": round(self.Income, 2),
451
+ "Savings": round(self.Savings, 2),
452
+ "LifeSatisfaction": round(self.LifeSatisfaction, 3),
453
+ "Friends": self._CountRelationshipsByType(["friend", "close_friend"]),
454
+ "Partner": self.RomanticPartner.Name if self.RomanticPartner else None,
455
+ "Children": len(self.Children),
456
+ "GoalsAchieved": sum(1 for G in self.Goals if G.Achieved),
457
+ }
458
+
459
+ def __repr__(self):
460
+ Status = f"{'alive' if self.IsAlive else 'deceased'}, age={self.Age}"
461
+ Job = self.Job.Role if self.Job else "unemployed"
462
+ return f"Person({self.Name}, {Status}, job={Job}, happiness={self.LifeSatisfaction:.2f})"
463
+
464
+ def __str__(self):
465
+ Status = f"{'Alive' if self.IsAlive else 'Deceased'}, Age {self.Age}"
466
+ JobStr = f", Job: {self.Job.Role}" if self.Job else ", Unemployed"
467
+ PartnerStr = f", Partner: {self.RomanticPartner.Name}" if self.RomanticPartner else ""
468
+ return f"{self.Name} ({Status}{JobStr}{PartnerStr})"