RayMelius Claude Sonnet 4.6 commited on
Commit
efc4478
·
1 Parent(s): 60bea97

Distribute agents across full map, add children, add corner buildings

Browse files

- web/index.html: GEN_HOUSE_SLOTS (52 positions across whole map) replaces
old hash-to-bottom-strip logic; 4 corner apartments + diner/pharmacy added
to LOCATION_POSITIONS
- config/city.yaml: 4 corner apartment blocks (apt_northeast/northwest/
southeast/southwest) and Blue Moon Diner + SociMed Pharmacy added
- generator.py: 20% of generated agents are children (age 8-17) with
elementary/middle/high school student occupations and child backstories

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. config/city.yaml +48 -0
  2. src/soci/agents/generator.py +20 -4
  3. web/index.html +47 -11
config/city.yaml CHANGED
@@ -253,3 +253,51 @@ locations:
253
  description: A tree-lined avenue on the west side, near the school and sports fields.
254
  capacity: 40
255
  connected_to: [house_kai, school, church, sports_field, street_north, street_south]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  description: A tree-lined avenue on the west side, near the school and sports fields.
254
  capacity: 40
255
  connected_to: [house_kai, school, church, sports_field, street_north, street_south]
256
+
257
+ # ===================
258
+ # CORNER APARTMENT BLOCKS --- expand residential to all four quadrants
259
+ # ===================
260
+ - id: apt_northeast
261
+ name: "Eastview Terrace"
262
+ zone: residential
263
+ description: A modern apartment block in the northeast corner, popular with hospital staff and factory workers.
264
+ capacity: 12
265
+ connected_to: [street_east, hospital, office_tower, diner]
266
+
267
+ - id: apt_northwest
268
+ name: "Hilltop Gardens"
269
+ zone: residential
270
+ description: A quiet residential complex on the northwest hilltop, overlooking the school and church.
271
+ capacity: 12
272
+ connected_to: [street_west, school, church]
273
+
274
+ - id: apt_southeast
275
+ name: "Riverside Commons"
276
+ zone: residential
277
+ description: A lively apartment complex near the sports field and factory district, popular with young workers.
278
+ capacity: 12
279
+ connected_to: [street_south, factory, sports_field, library]
280
+
281
+ - id: apt_southwest
282
+ name: "Orchard Hill Flats"
283
+ zone: residential
284
+ description: A peaceful residential block on the southwest edge of town, close to the library and park.
285
+ capacity: 12
286
+ connected_to: [street_south, library, sports_field]
287
+
288
+ # ===================
289
+ # NEW COMMERCIAL --- east side balance
290
+ # ===================
291
+ - id: diner
292
+ name: "Blue Moon Diner"
293
+ zone: commercial
294
+ description: A classic diner open early to late. Strong coffee, big plates, and regulars who treat it like a second home.
295
+ capacity: 12
296
+ connected_to: [street_east, hospital, factory, apt_northeast]
297
+
298
+ - id: pharmacy
299
+ name: "SociMed Pharmacy"
300
+ zone: commercial
301
+ description: A neighborhood pharmacy where the pharmacist knows everyone by name and stocks good candy.
302
+ capacity: 8
303
+ connected_to: [street_south, library, apartment_block_2, apt_southwest]
src/soci/agents/generator.py CHANGED
@@ -112,7 +112,7 @@ QUIRKS_POOL = [
112
 
113
  # Occupation categories for schedule variation
114
  EVENING_SHIFT_JOBS = {"bartender", "chef", "waiter", "barista"}
115
- STUDENT_OCCUPATIONS = {"college student"}
116
  RETIRED_OCCUPATIONS = {"retired"}
117
  PHYSICAL_JOBS = {"mechanic", "electrician", "plumber", "construction worker", "personal trainer"}
118
 
@@ -150,6 +150,12 @@ def _pick_name(gender: str, used_names: set[str]) -> str:
150
 
151
  def _pick_occupation(age: int) -> tuple[str, str | None]:
152
  """Pick occupation based on age. Returns (title, work_location_id)."""
 
 
 
 
 
 
153
  if age >= 65 and random.random() < 0.7:
154
  return "retired", None
155
  if 18 <= age <= 22 and random.random() < 0.6:
@@ -239,7 +245,13 @@ def _generate_background(name: str, age: int, occupation: str, traits: dict[str,
239
  first = name.split()[0]
240
 
241
  # Age-based life stage
242
- if age <= 22:
 
 
 
 
 
 
243
  stage = f"{first} is a {age}-year-old finding their way in life"
244
  elif age <= 35:
245
  stage = f"{first} is a {age}-year-old building their career"
@@ -251,7 +263,11 @@ def _generate_background(name: str, age: int, occupation: str, traits: dict[str,
251
  stage = f"{first} is a {age}-year-old enjoying their golden years"
252
 
253
  # Occupation context
254
- if occupation == "retired":
 
 
 
 
255
  job_part = "After decades of work, they now fill their days with hobbies and neighborhood life."
256
  elif occupation == "college student":
257
  subjects = random.choice([
@@ -328,7 +344,7 @@ def generate_personas(count: int, city: City) -> list[Persona]:
328
  for i in range(count):
329
  gender = _pick_gender()
330
  name = _pick_name(gender, used_names)
331
- age = random.randint(18, 75)
332
  occupation, work_location_id = _pick_occupation(age)
333
  traits = _generate_traits()
334
  values = _pick_values(traits)
 
112
 
113
  # Occupation categories for schedule variation
114
  EVENING_SHIFT_JOBS = {"bartender", "chef", "waiter", "barista"}
115
+ STUDENT_OCCUPATIONS = {"college student", "elementary student", "middle school student", "high school student"}
116
  RETIRED_OCCUPATIONS = {"retired"}
117
  PHYSICAL_JOBS = {"mechanic", "electrician", "plumber", "construction worker", "personal trainer"}
118
 
 
150
 
151
  def _pick_occupation(age: int) -> tuple[str, str | None]:
152
  """Pick occupation based on age. Returns (title, work_location_id)."""
153
+ if age <= 11:
154
+ return "elementary student", "school"
155
+ if age <= 14:
156
+ return "middle school student", "school"
157
+ if age <= 17:
158
+ return "high school student", "school"
159
  if age >= 65 and random.random() < 0.7:
160
  return "retired", None
161
  if 18 <= age <= 22 and random.random() < 0.6:
 
245
  first = name.split()[0]
246
 
247
  # Age-based life stage
248
+ if age <= 11:
249
+ stage = f"{first} is {age} years old and attends Soci Elementary School"
250
+ elif age <= 14:
251
+ stage = f"{first} is {age} years old and is in middle school"
252
+ elif age <= 17:
253
+ stage = f"{first} is {age} years old and is a high schooler at Soci School"
254
+ elif age <= 22:
255
  stage = f"{first} is a {age}-year-old finding their way in life"
256
  elif age <= 35:
257
  stage = f"{first} is a {age}-year-old building their career"
 
263
  stage = f"{first} is a {age}-year-old enjoying their golden years"
264
 
265
  # Occupation context
266
+ if occupation == "elementary student":
267
+ job_part = "They love recess, have strong opinions about their favourite subjects, and make friends easily."
268
+ elif occupation in ("middle school student", "high school student"):
269
+ job_part = "They're navigating homework, friendships, and figuring out who they are."
270
+ elif occupation == "retired":
271
  job_part = "After decades of work, they now fill their days with hobbies and neighborhood life."
272
  elif occupation == "college student":
273
  subjects = random.choice([
 
344
  for i in range(count):
345
  gender = _pick_gender()
346
  name = _pick_name(gender, used_names)
347
+ age = random.randint(8, 17) if random.random() < 0.20 else random.randint(18, 75)
348
  occupation, work_location_id = _pick_occupation(age)
349
  traits = _generate_traits()
350
  values = _pick_values(traits)
web/index.html CHANGED
@@ -257,6 +257,23 @@ const ROADS = [
257
  { x1: 0.70, y1: 0.58, x2: 0.70, y2: 0.72, width: 6 },
258
  ];
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  // Building positions — spread across larger grid
261
  const LOCATION_POSITIONS = {
262
  // === RESIDENTIAL — Row 1: North houses ===
@@ -300,6 +317,14 @@ const LOCATION_POSITIONS = {
300
  town_square: { x: 0.50, y: 0.50, type: 'square', label: 'Town Square' },
301
  sports_field: { x: 0.22, y: 0.78, type: 'sports', label: 'Sports Field' },
302
 
 
 
 
 
 
 
 
 
303
  // === STREETS ===
304
  street_north: { x: 0.50, y: 0.28, type: 'street', label: 'N. Main St' },
305
  street_south: { x: 0.50, y: 0.58, type: 'street', label: 'S. Main St' },
@@ -657,22 +682,33 @@ function draw() {
657
  let _genPosCache = {};
658
  function getEffectivePositions() {
659
  const result = {...LOCATION_POSITIONS};
660
- // Add any locations from server data that we don't have positions for
661
  for (const locId of Object.keys(locations)) {
662
  if (!result[locId]) {
663
  if (!_genPosCache[locId]) {
664
- // Hash-based position in residential rows
665
  let h = 0;
666
  for (let i = 0; i < locId.length; i++) h = ((h << 5) - h + locId.charCodeAt(i)) | 0;
667
- const col = ((h >>> 0) % 12) / 12;
668
- const row = ((h >>> 4) % 3);
669
- const rowY = [0.82, 0.86, 0.90][row];
670
- _genPosCache[locId] = {
671
- x: 0.06 + col * 0.88,
672
- y: rowY + ((h >>> 8) % 3) * 0.01,
673
- type: 'house',
674
- label: (locations[locId]?.name || locId).slice(0, 14),
675
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
676
  }
677
  result[locId] = _genPosCache[locId];
678
  }
 
257
  { x1: 0.70, y1: 0.58, x2: 0.70, y2: 0.72, width: 6 },
258
  ];
259
 
260
+ // Slot grid for procedurally generated houses — 52 positions covering the whole map
261
+ const GEN_HOUSE_SLOTS = (function() {
262
+ const s = [];
263
+ for (let i = 0; i < 10; i++) s.push({x: 0.04 + i * 0.092, y: 0.06 + (i % 3) * 0.025});
264
+ for (let i = 0; i < 7; i++) s.push({x: 0.02, y: 0.19 + i * 0.10});
265
+ for (let i = 0; i < 7; i++) s.push({x: 0.97, y: 0.19 + i * 0.10});
266
+ for (let i = 0; i < 10; i++) s.push({x: 0.04 + i * 0.092, y: 0.88 + (i % 3) * 0.025});
267
+ const interior = [
268
+ [0.13,0.14],[0.28,0.14],[0.44,0.13],[0.58,0.14],[0.73,0.13],[0.87,0.14],
269
+ [0.13,0.80],[0.28,0.80],[0.44,0.81],[0.58,0.80],[0.73,0.81],[0.87,0.80],
270
+ [0.93,0.22],[0.93,0.45],[0.93,0.57],[0.93,0.74],
271
+ [0.03,0.22],[0.03,0.45],[0.03,0.57],[0.03,0.74],
272
+ ];
273
+ for (const [x,y] of interior) s.push({x,y});
274
+ return s; // 52 slots
275
+ })();
276
+
277
  // Building positions — spread across larger grid
278
  const LOCATION_POSITIONS = {
279
  // === RESIDENTIAL — Row 1: North houses ===
 
317
  town_square: { x: 0.50, y: 0.50, type: 'square', label: 'Town Square' },
318
  sports_field: { x: 0.22, y: 0.78, type: 'sports', label: 'Sports Field' },
319
 
320
+ // === NEW BUILDINGS (corner apartments + east-side commercial) ===
321
+ apt_northeast: { x: 0.93, y: 0.10, type: 'apartment', label: 'Eastview Terrace' },
322
+ apt_northwest: { x: 0.04, y: 0.10, type: 'apartment', label: 'Hilltop Gardens' },
323
+ apt_southeast: { x: 0.93, y: 0.86, type: 'apartment', label: 'Riverside Commons' },
324
+ apt_southwest: { x: 0.04, y: 0.86, type: 'apartment', label: 'Orchard Hill Flats' },
325
+ diner: { x: 0.92, y: 0.36, type: 'shop', label: 'Blue Moon Diner' },
326
+ pharmacy: { x: 0.35, y: 0.78, type: 'shop', label: 'SociMed Pharmacy' },
327
+
328
  // === STREETS ===
329
  street_north: { x: 0.50, y: 0.28, type: 'street', label: 'N. Main St' },
330
  street_south: { x: 0.50, y: 0.58, type: 'street', label: 'S. Main St' },
 
682
  let _genPosCache = {};
683
  function getEffectivePositions() {
684
  const result = {...LOCATION_POSITIONS};
 
685
  for (const locId of Object.keys(locations)) {
686
  if (!result[locId]) {
687
  if (!_genPosCache[locId]) {
 
688
  let h = 0;
689
  for (let i = 0; i < locId.length; i++) h = ((h << 5) - h + locId.charCodeAt(i)) | 0;
690
+ const genMatch = locId.match(/house_gen_(\d+)/);
691
+ if (genMatch) {
692
+ // Deterministic slot assignment: house_gen_01 → slot 0, house_gen_02 → slot 1,
693
+ const slotIdx = (parseInt(genMatch[1]) - 1) % GEN_HOUSE_SLOTS.length;
694
+ const sl = GEN_HOUSE_SLOTS[slotIdx];
695
+ const jx = ((h >>> 8) & 0xf) * 0.004 - 0.028;
696
+ const jy = ((h >>> 12) & 0xf) * 0.004 - 0.028;
697
+ _genPosCache[locId] = {
698
+ x: Math.max(0.01, Math.min(0.99, sl.x + jx)),
699
+ y: Math.max(0.01, Math.min(0.99, sl.y + jy)),
700
+ type: 'house',
701
+ label: (locations[locId]?.name || locId).slice(0, 14),
702
+ };
703
+ } else {
704
+ // Unknown location: spread evenly across the whole map via hash
705
+ _genPosCache[locId] = {
706
+ x: 0.05 + ((h >>> 0) % 18) / 18 * 0.90,
707
+ y: 0.05 + ((h >>> 4) % 16) / 16 * 0.90,
708
+ type: 'house',
709
+ label: (locations[locId]?.name || locId).slice(0, 14),
710
+ };
711
+ }
712
  }
713
  result[locId] = _genPosCache[locId];
714
  }