1use crate::random::Random;
7use crate::scheduler::{seconds, ticks};
8
9pub mod activity {
13 pub const EAT: u32 = 0;
15 pub const WARM: u32 = 1;
17 pub const EMERGENCY: u32 = 2;
19 pub const CLOTHING: u32 = 3;
21 pub const HERBS: u32 = 4;
23 pub const DOCTOR: u32 = 5;
25 pub const HOME_FOOD: u32 = 6;
27 pub const HOME_FUEL: u32 = 7;
29 pub const TOOL: u32 = 8;
31 pub const WORK: u32 = 9;
33 pub const SCHOOL: u32 = 10;
35 pub const PLAY: u32 = 11;
37 pub const IDLE: u32 = 12;
39 pub const DROP_OFF: u32 = 13;
41 pub const EAT_URGENT: u32 = 14;
43 pub const WARM_URGENT: u32 = 15;
45}
46use activity::*;
47
48const LOITER_UNHAPPY: f32 = 75.0;
50const LOITER_SAD: f32 = 300.0;
51const LOITER_ANYWAY: f32 = 1800.0;
52const CARRY_LONG: f32 = 300.0;
54
55#[derive(Debug, Clone, Copy, Default)]
57pub struct CitizenView {
58 pub sick: bool,
61 pub doctor: bool,
63 pub hunger: f32,
66 pub cold: f32,
68 pub health: f32,
70 pub health_flag: bool,
72 pub happiness: f32,
74 pub age: f32,
77 pub school: bool,
79 pub home: bool,
81 pub clothing: i32,
83 pub tool: i32,
85 pub emergency: bool,
90 pub home_food: bool,
94 pub home_fuel: bool,
96 pub spouse_activity: Option<u32>,
99 pub carrying: i32,
103}
104
105#[derive(Debug, Clone, Copy, Default)]
107pub struct Thresholds {
108 pub adult_age: f32,
111 pub health_low: f32,
114 pub happy_low: f32,
117 pub happy_unhappy: f32,
120 pub hungry: f32,
123 pub hungry_urgent: f32,
126 pub cold: f32,
129 pub cold_urgent: f32,
132}
133
134pub type Candidate = (u32, i32);
136
137#[derive(Debug, Clone)]
140pub struct CitizenAi {
141 pub current: u32,
144 pub queued: Option<u32>,
146 pub eating: bool,
149 pub warming: bool,
151 pub cooldowns: [i64; 16],
154 pub since_loiter: i64,
157 pub carrying_time: i64,
159}
160
161impl Default for CitizenAi {
162 fn default() -> Self {
163 CitizenAi {
164 current: IDLE,
165 queued: None,
166 eating: false,
167 warming: false,
168 cooldowns: [0; 16],
169 since_loiter: 0,
170 carrying_time: 0,
171 }
172 }
173}
174
175pub fn candidates(
180 ai: &CitizenAi,
181 c: &CitizenView,
182 d: &Thresholds,
183 urgent_only: bool,
184) -> (Vec<Candidate>, bool) {
185 let (list, early) = candidate_list(ai, c, d, urgent_only);
186 (list, !early)
187}
188
189fn candidate_list(
190 ai: &CitizenAi,
191 c: &CitizenView,
192 d: &Thresholds,
193 urgent_only: bool,
194) -> (Vec<Candidate>, bool) {
195 let mut out = Vec::new();
196 if c.sick && c.doctor {
197 out.push((DOCTOR, 6));
198 return (out, true);
199 }
200 if c.hunger > d.hungry_urgent {
201 out.push((EAT_URGENT, 7));
202 }
203 if c.cold > d.cold_urgent {
204 out.push((WARM_URGENT, 7));
205 }
206 if c.hunger > d.hungry {
207 out.push((EAT, 6));
208 }
209 if c.cold > d.cold {
210 out.push((WARM, 6));
211 }
212 if !c.sick {
213 if c.emergency {
214 out.push((EMERGENCY, 7));
215 }
216 if c.clothing == 0 {
217 out.push((CLOTHING, 3));
218 }
219 if c.health < d.health_low && !c.health_flag {
220 out.push((HERBS, 3));
221 }
222 } else {
223 out.push((DOCTOR, 6));
224 }
225 if urgent_only {
226 return (out, true);
227 }
228 let loiter = seconds(ai.since_loiter);
229 let restless = (c.happiness < d.happy_unhappy && loiter > LOITER_UNHAPPY)
230 || (c.happiness < d.happy_low && loiter > LOITER_SAD)
231 || loiter > LOITER_ANYWAY;
232 out.push((IDLE, if restless { 2 } else { 0 }));
233 if c.age < d.adult_age || c.school {
234 if !c.school {
235 if !c.sick {
236 out.push((PLAY, 0));
237 }
238 if !c.home_food {
239 out.push((HOME_FOOD, 4));
240 }
241 if c.home_fuel {
242 return (out, false);
243 }
244 out.push((HOME_FUEL, 4));
245 } else if !c.sick {
246 out.push((SCHOOL, 2));
247 }
248 return (out, false);
249 }
250 if c.home {
251 if !c.home_food && c.spouse_activity != Some(1) {
252 out.push((HOME_FOOD, if c.hunger > d.hungry_urgent { 6 } else { 4 }));
253 }
254 if !c.home_fuel && c.spouse_activity != Some(2) {
255 out.push((HOME_FUEL, if c.cold > d.cold_urgent { 6 } else { 4 }));
256 }
257 }
258 if c.sick {
259 return (out, false);
260 }
261 if c.tool < 1 {
262 out.push((TOOL, 5));
263 }
264 out.push((WORK, 2));
265 if c.carrying >= 1 {
266 let long = ticks(CARRY_LONG) < ai.carrying_time;
267 out.push((DROP_OFF, if long { 3 } else { 1 }));
268 }
269 (out, false)
270}
271
272pub fn shuffle_and_sort(list: &mut [Candidate], rng: &mut Random) {
276 let n = list.len() as u32;
277 for i in 0..list.len() {
278 let j = rng.rand_below(n) as usize;
279 if j != i {
280 list.swap(i, j);
281 }
282 }
283 if list.len() > 1 {
284 crt_qsort(list);
285 }
286}
287
288pub fn crt_qsort(a: &mut [Candidate]) {
294 crt_qsort_by(a, |a: &Candidate, b: &Candidate| b.1 - a.1);
295}
296
297pub fn crt_qsort_less<T>(a: &mut [T], less: impl Fn(&T, &T) -> bool) {
304 crt_qsort_by(a, |x, y| {
305 if less(y, x) {
306 1
307 } else if less(x, y) {
308 -1
309 } else {
310 0
311 }
312 });
313}
314
315pub fn crt_qsort_by<T>(a: &mut [T], cmp: impl Fn(&T, &T) -> i32) {
317 const CUTOFF: usize = 8;
318 if a.len() < 2 {
319 return;
320 }
321 let mut stack: Vec<(usize, usize)> = Vec::new();
322 let (mut lo, mut hi) = (0usize, a.len() - 1);
323 loop {
324 let size = hi - lo + 1;
325 if size <= CUTOFF {
326 let mut h = hi;
328 while h > lo {
329 let mut max = lo;
330 for p in lo + 1..=h {
331 if cmp(&a[p], &a[max]) > 0 {
332 max = p;
333 }
334 }
335 a.swap(max, h);
336 h -= 1;
337 }
338 } else {
339 let mut mid = lo + size / 2;
340 if cmp(&a[lo], &a[mid]) > 0 {
341 a.swap(lo, mid);
342 }
343 if cmp(&a[lo], &a[hi]) > 0 {
344 a.swap(lo, hi);
345 }
346 if cmp(&a[mid], &a[hi]) > 0 {
347 a.swap(mid, hi);
348 }
349 let (mut loguy, mut higuy) = (lo as isize, hi as isize);
350 let m = |mid: usize| mid as isize;
351 loop {
352 if m(mid) > loguy {
353 loop {
354 loguy += 1;
355 if !(loguy < m(mid) && cmp(&a[loguy as usize], &a[mid]) <= 0) {
356 break;
357 }
358 }
359 }
360 if m(mid) <= loguy {
361 loop {
362 loguy += 1;
363 if !(loguy <= hi as isize && cmp(&a[loguy as usize], &a[mid]) <= 0) {
364 break;
365 }
366 }
367 }
368 loop {
369 higuy -= 1;
370 if !(higuy > m(mid) && cmp(&a[higuy as usize], &a[mid]) > 0) {
371 break;
372 }
373 }
374 if higuy < loguy {
375 break;
376 }
377 a.swap(loguy as usize, higuy as usize);
378 if m(mid) == higuy {
379 mid = loguy as usize;
380 }
381 }
382 higuy += 1;
383 if m(mid) < higuy {
384 loop {
385 higuy -= 1;
386 if !(higuy > m(mid) && cmp(&a[higuy as usize], &a[mid]) == 0) {
387 break;
388 }
389 }
390 }
391 if m(mid) >= higuy {
392 loop {
393 higuy -= 1;
394 if !(higuy > lo as isize && cmp(&a[higuy as usize], &a[mid]) == 0) {
395 break;
396 }
397 }
398 }
399 if higuy - lo as isize >= hi as isize - loguy {
400 if (lo as isize) < higuy {
401 stack.push((lo, higuy as usize));
402 }
403 if loguy < hi as isize {
404 lo = loguy as usize;
405 continue;
406 }
407 } else {
408 if loguy < hi as isize {
409 stack.push((loguy as usize, hi));
410 }
411 if (lo as isize) < higuy {
412 hi = higuy as usize;
413 continue;
414 }
415 }
416 }
417 match stack.pop() {
418 Some((l, h)) => {
419 lo = l;
420 hi = h;
421 }
422 None => return,
423 }
424 }
425}
426
427impl CitizenAi {
428 pub fn tick(&mut self, dt: i64, carrying: i32) {
432 for c in &mut self.cooldowns {
433 *c = (*c - dt).max(0);
434 }
435 self.since_loiter += dt;
436 self.carrying_time += dt;
437 if carrying == 0 {
438 self.carrying_time = 0;
439 }
440 }
441
442 pub fn update(
449 &mut self,
450 dt: i64,
451 view: &CitizenView,
452 d: &Thresholds,
453 rng: &mut Random,
454 busy: impl Fn() -> bool,
455 mut run: impl FnMut(&mut CitizenAi, u32, &mut Random) -> bool,
456 ) {
457 self.tick(dt, view.carrying);
458 let urgent_running = (self.eating && self.current == EAT_URGENT)
459 || (self.warming && self.current == WARM_URGENT);
460 let in_urgent = self.current == EAT_URGENT || self.current == WARM_URGENT;
461 if view.hunger <= d.hungry_urgent
462 || urgent_running
463 || in_urgent
464 || self.cooldowns[EAT_URGENT as usize] > 0
465 {
466 if self.eating && view.hunger <= d.hungry_urgent {
467 self.eating = false;
468 }
469 } else {
470 self.choose(view, d, true, rng, &mut run);
471 self.eating = self.current == EAT_URGENT;
472 }
473 let in_urgent = self.current == EAT_URGENT || self.current == WARM_URGENT;
475 if view.cold <= d.cold_urgent
476 || urgent_running
477 || in_urgent
478 || self.cooldowns[WARM_URGENT as usize] > 0
479 {
480 if self.warming && view.cold <= d.cold_urgent {
481 self.warming = false;
482 }
483 } else {
484 self.choose(view, d, true, rng, &mut run);
485 self.warming = self.current == WARM_URGENT;
486 }
487 if !self.eating && !self.warming && view.emergency && self.current != EMERGENCY {
488 self.choose(view, d, true, rng, &mut run);
489 }
490 if busy() {
491 return;
492 }
493 self.choose(view, d, false, rng, &mut run);
494 }
495
496 pub fn choose(
501 &mut self,
502 view: &CitizenView,
503 d: &Thresholds,
504 urgent_only: bool,
505 rng: &mut Random,
506 mut run: impl FnMut(&mut CitizenAi, u32, &mut Random) -> bool,
507 ) {
508 if let Some(q) = self.queued {
509 if self.dispatch(q, &mut run, rng) {
510 self.current = q;
511 self.queued = None;
512 return;
513 }
514 }
515 let (mut list, shuffle) = candidates(self, view, d, urgent_only);
516 if shuffle {
517 shuffle_and_sort(&mut list, rng);
518 }
519 for (activity, _) in list {
520 if self.dispatch(activity, &mut run, rng) {
521 self.current = activity;
522 break;
523 }
524 }
525 }
526
527 fn dispatch(
529 &mut self,
530 activity: u32,
531 run: &mut impl FnMut(&mut CitizenAi, u32, &mut Random) -> bool,
532 rng: &mut Random,
533 ) -> bool {
534 if self.cooldowns[activity as usize] >= 1 {
535 return false;
536 }
537 run(self, activity, rng)
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 #[test]
546 fn qsort_orders_by_priority_descending() {
547 let mut v: Vec<Candidate> = (0..20).map(|i| (i, (i as i32 * 7) % 5)).collect();
548 crt_qsort(&mut v);
549 assert!(v.windows(2).all(|w| w[0].1 >= w[1].1), "{v:?}");
550 let mut v: Vec<Candidate> = vec![(12, 0), (8, 5), (3, 3), (9, 2)];
551 crt_qsort(&mut v);
552 assert_eq!(v, vec![(8, 5), (3, 3), (9, 2), (12, 0)]);
553 }
554
555 #[test]
556 fn new_adult_candidates() {
557 let ai = CitizenAi::default();
558 let view = CitizenView {
559 age: 20.0,
560 health: 1.0,
561 ..Default::default()
562 };
563 let d = Thresholds {
564 adult_age: 10.0,
565 hungry: 50.0,
566 hungry_urgent: 75.0,
567 cold: 50.0,
568 cold_urgent: 75.0,
569 ..Default::default()
570 };
571 let (c, shuffle) = candidates(&ai, &view, &d, false);
572 assert!(shuffle);
573 assert_eq!(c, vec![(CLOTHING, 3), (IDLE, 0), (TOOL, 5), (WORK, 2)]);
574 }
575}