src/jobs/tests.rs 34 K · 1065 lines · raw · history

1 use std::collections::VecDeque;
2
3 use super::*;
4 use crate::{
5 animals::{Animal, AnimalKind},
6 colonist::follow_path,
7 food::Hunger,
8 items::STACK_LIMIT,
9 sim::{SimPos, sim_pos},
10 world::Terrain,
11 zones::NEW_STOCKPILE_SIZE,
12 };
13
14 const TICK: Duration = Duration::from_nanos(1_000_000_000 / 60);
15
16 fn test_app(map: WorldMap) -> App {
17 let mut app = App::new();
18 app.insert_resource(map)
19 .insert_resource(SimRng::new(1))
20 .init_resource::<Time>()
21 .init_resource::<Reservations>()
22 .init_resource::<ColonyGoals>()
23 .init_resource::<ItemAssets>()
24 .init_resource::<StructureAssets>()
25 .init_resource::<Blueprints>()
26 .init_resource::<LightMap>()
27 .init_resource::<SimTick>()
28 .init_resource::<Zones>()
29 .add_message::<Order>()
30 .add_message::<TileChanged>()
31 .add_systems(
32 Update,
33 (
34 handle_orders,
35 choose_jobs,
36 run_jobs,
37 follow_path,
38 crate::needs::update_rest,
39 )
40 .chain(),
41 );
42 app
43 }
44
45 fn spawn_colonist(app: &mut App, tile: UVec2) -> Entity {
46 app.world_mut()
47 .spawn((
48 Colonist {
49 name: "Test".into(),
50 },
51 TilePos(tile),
52 sim_pos(WorldMap::tile_center(tile)),
53 ))
54 .id()
55 }
56
57 fn run_ticks(app: &mut App, ticks: u32) {
58 for _ in 0..ticks {
59 app.world_mut().resource_mut::<Time>().advance_by(TICK);
60 app.update();
61 }
62 }
63
64 fn map_with_trees(trees: &[UVec2]) -> WorldMap {
65 let mut map = WorldMap::filled(UVec2::new(20, 20), Terrain::Grass);
66 for &t in trees {
67 map.set_plant(t, Some(Plant::Tree));
68 }
69 map
70 }
71
72 fn wood_on_map(app: &mut App) -> u32 {
73 let world = app.world_mut();
74 let items: Vec<Item> = world.query::<&Item>().iter(world).copied().collect();
75 stock(items.iter(), ItemKind::Wood)
76 }
77
78 fn items_at(app: &mut App) -> Vec<(UVec2, u32)> {
79 let world = app.world_mut();
80 let mut items: Vec<(UVec2, u32)> = world
81 .query::<(&Item, &TilePos)>()
82 .iter(world)
83 .map(|(item, pos)| (pos.0, item.count))
84 .collect();
85 items.sort_by_key(|(p, _)| (p.y, p.x));
86 items
87 }
88
89 fn put_wood(app: &mut App, tile: UVec2, count: u32) {
90 app.world_mut().spawn((
91 Item {
92 kind: ItemKind::Wood,
93 count,
94 },
95 TilePos(tile),
96 ));
97 }
98
99 fn set_goals(app: &mut App, goals: &[(ItemKind, u32)]) {
100 *app.world_mut().resource_mut::<ColonyGoals>() = ColonyGoals(goals.iter().copied().collect());
101 }
102
103 fn stockpile_size(app: &App) -> usize {
104 let map = app.world().resource::<WorldMap>();
105 let zones = app.world().resource::<Zones>();
106 map.positions().filter(|&p| zones.is_stockpile(p)).count()
107 }
108
109 fn stored(app: &mut App) -> u32 {
110 let zones = app.world().resource::<Zones>().clone();
111 items_at(app)
112 .iter()
113 .filter(|(p, _)| zones.is_stockpile(*p))
114 .map(|(_, c)| c)
115 .sum()
116 }
117
118 fn put_beds(app: &mut App, count: u32) {
119 for x in 0..count {
120 app.world_mut().spawn((
121 Structure {
122 kind: StructureKind::Bed,
123 },
124 TilePos(UVec2::new(19 - x, 19)),
125 ));
126 }
127 }
128
129 fn hauling_app(stockpile: (IVec2, IVec2)) -> App {
130 let mut app = test_app(map_with_trees(&[]));
131 put_beds(&mut app, 2);
132 set_goals(&mut app, &[]);
133 let map = app.world().resource::<WorldMap>().clone();
134 app.world_mut()
135 .resource_mut::<Zones>()
136 .add_stockpile(&map, stockpile.0, stockpile.1);
137 app
138 }
139
140 fn carrying(app: &App, colonist: Entity) -> Option<Item> {
141 app.world().get::<Carrying>(colonist).unwrap().0
142 }
143
144 fn job(app: &App, colonist: Entity) -> Option<Job> {
145 app.world().get::<CurrentJob>(colonist).unwrap().0
146 }
147
148 #[test]
149 fn idle_colonists_cut_trees_when_the_colony_needs_wood() {
150 let tree = UVec2::new(8, 5);
151 let mut app = test_app(map_with_trees(&[tree]));
152 spawn_colonist(&mut app, UVec2::new(2, 5));
153 run_ticks(&mut app, 60 * 15);
154 assert_eq!(
155 app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
156 None
157 );
158 assert_eq!(wood_on_map(&mut app), 10);
159 assert!(app.world().resource::<Reservations>().0.is_empty());
160 }
161
162 #[test]
163 fn colonists_leave_trees_alone_when_there_is_enough_wood() {
164 let tree = UVec2::new(8, 5);
165 let mut app = test_app(map_with_trees(&[tree]));
166 set_goals(&mut app, &[]);
167 spawn_colonist(&mut app, UVec2::new(2, 5));
168 run_ticks(&mut app, 60 * 15);
169 assert_eq!(
170 app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
171 Some(Plant::Tree)
172 );
173 }
174
175 #[test]
176 fn colonists_never_pick_the_same_tree() {
177 let trees = [UVec2::new(10, 10), UVec2::new(10, 11)];
178 let mut app = test_app(map_with_trees(&trees));
179 let a = spawn_colonist(&mut app, UVec2::new(5, 10));
180 let b = spawn_colonist(&mut app, UVec2::new(5, 11));
181 run_ticks(&mut app, 61);
182 let target = |c| match job(&app, c) {
183 Some(Job {
184 task: Task::Work { site, .. },
185 ..
186 }) => site,
187 other => panic!("expected chop job, got {other:?}"),
188 };
189 assert_ne!(target(a), target(b));
190 }
191
192 // Each tree or rock being worked will add its yield; counting that as material on the way
193 // keeps the whole colony from felling a forest when only a little more wood is needed.
194 #[test]
195 fn work_in_progress_counts_towards_goals() {
196 let goals = ColonyGoals(HashMap::from([(ItemKind::Wood, 25)]));
197 assert!(goals.wanted(ItemKind::Wood, 10, 10));
198 assert!(!goals.wanted(ItemKind::Wood, 10, 20));
199 assert!(!goals.wanted(ItemKind::Wood, 30, 0));
200 assert!(!goals.wanted(ItemKind::Gold, 0, 0));
201 }
202
203 #[test]
204 fn ordered_chop_ignores_the_wood_goal() {
205 let tree = UVec2::new(8, 5);
206 let mut app = test_app(map_with_trees(&[tree]));
207 set_goals(&mut app, &[]);
208 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
209 app.world_mut().write_message(Order {
210 colonist,
211 tile: tree,
212 });
213 run_ticks(&mut app, 60 * 15);
214 assert_eq!(
215 app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
216 None
217 );
218 }
219
220 #[test]
221 fn ordering_a_move_cancels_the_current_job_and_frees_its_tree() {
222 let tree = UVec2::new(15, 5);
223 let mut app = test_app(map_with_trees(&[tree]));
224 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
225 run_ticks(&mut app, 61);
226 assert!(matches!(
227 job(&app, colonist),
228 Some(Job {
229 task: Task::Work { .. },
230 ..
231 })
232 ));
233 app.world_mut().write_message(Order {
234 colonist,
235 tile: UVec2::new(2, 15),
236 });
237 run_ticks(&mut app, 1);
238 assert_eq!(
239 job(&app, colonist).map(|j| j.task),
240 Some(Task::MoveTo(UVec2::new(2, 15)))
241 );
242 assert!(
243 app.world()
244 .resource::<Reservations>()
245 .is_free(Target::Site(tree))
246 );
247 }
248
249 #[test]
250 fn idle_colonists_haul_loose_items_into_the_stockpile() {
251 let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
252 put_wood(&mut app, UVec2::new(5, 5), 20);
253 spawn_colonist(&mut app, UVec2::new(2, 5));
254 run_ticks(&mut app, 60 * 15);
255 assert_eq!(items_at(&mut app), vec![(UVec2::new(15, 15), 20)]);
256 }
257
258 #[test]
259 fn large_stacks_take_several_trips_and_spill_into_more_stockpile_tiles() {
260 let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
261 put_wood(&mut app, UVec2::new(5, 5), STACK_LIMIT + 10);
262 spawn_colonist(&mut app, UVec2::new(2, 5));
263 run_ticks(&mut app, 60 * 40);
264 let stacks = items_at(&mut app);
265 assert_eq!(stored(&mut app), STACK_LIMIT + 10);
266 assert!(stacks.len() >= 2);
267 assert!(stacks.iter().all(|(_, count)| *count <= STACK_LIMIT));
268 }
269
270 #[test]
271 fn items_already_in_a_stockpile_stay_put() {
272 let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(16, 16)));
273 put_wood(&mut app, UVec2::new(5, 5), 20);
274 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
275 run_ticks(&mut app, 60 * 5);
276 assert_eq!(items_at(&mut app), vec![(UVec2::new(5, 5), 20)]);
277 assert_eq!(carrying(&app, colonist), None);
278 }
279
280 #[test]
281 fn colonists_found_a_stockpile_when_items_need_storing() {
282 let mut app = test_app(map_with_trees(&[]));
283 put_beds(&mut app, 1);
284 set_goals(&mut app, &[]);
285 put_wood(&mut app, UVec2::new(5, 5), 20);
286 spawn_colonist(&mut app, UVec2::new(2, 5));
287 run_ticks(&mut app, 60 * 15);
288 assert_eq!(
289 stockpile_size(&app),
290 (NEW_STOCKPILE_SIZE * NEW_STOCKPILE_SIZE) as usize
291 );
292 assert_eq!(stored(&mut app), 20);
293 }
294
295 #[test]
296 fn no_stockpile_is_founded_while_there_is_nothing_to_store() {
297 let mut app = test_app(map_with_trees(&[]));
298 set_goals(&mut app, &[]);
299 spawn_colonist(&mut app, UVec2::new(2, 5));
300 run_ticks(&mut app, 60 * 5);
301 assert_eq!(stockpile_size(&app), 0);
302 }
303
304 #[test]
305 fn two_colonists_never_haul_the_same_stack() {
306 let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(16, 16)));
307 put_wood(&mut app, UVec2::new(5, 5), 10);
308 put_wood(&mut app, UVec2::new(5, 6), 10);
309 let a = spawn_colonist(&mut app, UVec2::new(2, 5));
310 let b = spawn_colonist(&mut app, UVec2::new(2, 6));
311 run_ticks(&mut app, 61);
312 let pickup = |c| match job(&app, c).map(|j| j.task) {
313 Some(Task::Haul { pickup, .. }) => pickup,
314 other => panic!("expected haul, got {other:?}"),
315 };
316 assert_ne!(pickup(a), pickup(b));
317 }
318
319 // Orders interrupt whatever a colonist is doing, but whatever it is carrying stays in its
320 // hands; once it is idle again it finishes delivering it instead of dropping it anywhere.
321 #[test]
322 fn interrupted_haulers_deliver_their_load_later() {
323 let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
324 put_wood(&mut app, UVec2::new(4, 5), 20);
325 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
326 let mut ticks = 0;
327 while carrying(&app, colonist).is_none() && ticks < 60 * 10 {
328 run_ticks(&mut app, 1);
329 ticks += 1;
330 }
331 app.world_mut().write_message(Order {
332 colonist,
333 tile: UVec2::new(2, 2),
334 });
335 run_ticks(&mut app, 60 * 3);
336 assert!(carrying(&app, colonist).is_some());
337 run_ticks(&mut app, 60 * 20);
338 assert_eq!(carrying(&app, colonist), None);
339 assert_eq!(stored(&mut app), 20);
340 }
341
342 #[test]
343 fn right_clicking_loose_items_orders_a_haul() {
344 let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(15, 15)));
345 set_goals(&mut app, &[(ItemKind::Wood, 1000)]);
346 put_wood(&mut app, UVec2::new(5, 5), 20);
347 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
348 app.world_mut().write_message(Order {
349 colonist,
350 tile: UVec2::new(5, 5),
351 });
352 run_ticks(&mut app, 1);
353 assert!(matches!(
354 job(&app, colonist),
355 Some(Job {
356 task: Task::Haul { .. },
357 ordered: true
358 })
359 ));
360 }
361
362 fn mining_map() -> WorldMap {
363 let mut map = WorldMap::filled(UVec2::new(20, 20), Terrain::Grass);
364 for y in 0..20 {
365 map.set(UVec2::new(10, y), Terrain::Stone);
366 }
367 map.set(UVec2::new(10, 5), Terrain::IronOre);
368 map
369 }
370
371 fn items_of(app: &mut App, kind: ItemKind) -> u32 {
372 let world = app.world_mut();
373 let items: Vec<Item> = world.query::<&Item>().iter(world).copied().collect();
374 stock(items.iter(), kind)
375 }
376
377 #[test]
378 fn colonists_mine_rock_the_colony_needs_and_leave_gravel() {
379 let mut app = test_app(mining_map());
380 set_goals(&mut app, &[(ItemKind::Stone, 10)]);
381 spawn_colonist(&mut app, UVec2::new(8, 12));
382 run_ticks(&mut app, 60 * 12);
383 assert_eq!(items_of(&mut app, ItemKind::Stone), 10);
384 assert_eq!(items_of(&mut app, ItemKind::Iron), 0);
385 let map = app.world().resource::<WorldMap>();
386 let gravel = map
387 .positions()
388 .filter(|p| map.get(p.as_ivec2()) == Some(Terrain::Gravel))
389 .count();
390 assert_eq!(gravel, 1);
391 }
392
393 #[test]
394 fn colonists_walk_past_rock_they_do_not_need() {
395 let mut app = test_app(mining_map());
396 set_goals(&mut app, &[(ItemKind::Iron, 10)]);
397 spawn_colonist(&mut app, UVec2::new(8, 15));
398 run_ticks(&mut app, 60 * 15);
399 assert_eq!(items_of(&mut app, ItemKind::Iron), 10);
400 assert_eq!(items_of(&mut app, ItemKind::Stone), 0);
401 assert_eq!(
402 app.world().resource::<WorldMap>().get(IVec2::new(10, 5)),
403 Some(Terrain::Gravel)
404 );
405 }
406
407 #[test]
408 fn right_clicking_rock_orders_mining_regardless_of_goals() {
409 let mut app = test_app(mining_map());
410 set_goals(&mut app, &[]);
411 let colonist = spawn_colonist(&mut app, UVec2::new(8, 12));
412 app.world_mut().write_message(Order {
413 colonist,
414 tile: UVec2::new(10, 12),
415 });
416 run_ticks(&mut app, 60 * 10);
417 assert_eq!(
418 app.world().resource::<WorldMap>().get(IVec2::new(10, 12)),
419 Some(Terrain::Gravel)
420 );
421 }
422
423 #[test]
424 fn colonists_expand_a_stockpile_that_is_nearly_full() {
425 let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(6, 5)));
426 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
427 run_ticks(&mut app, 61);
428 assert!(matches!(
429 job(&app, colonist).map(|j| j.task),
430 Some(Task::ExpandStockpile { .. })
431 ));
432 run_ticks(&mut app, 60 * 5);
433 assert_eq!(stockpile_size(&app), 2 + EXPANSION_TILES);
434 }
435
436 #[test]
437 fn roomy_stockpiles_are_left_alone() {
438 let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(8, 5)));
439 spawn_colonist(&mut app, UVec2::new(2, 5));
440 run_ticks(&mut app, 60 * 5);
441 assert_eq!(stockpile_size(&app), 4);
442 }
443
444 fn set_hour(app: &mut App, hour: u64) {
445 let ticks = (hour + 24 - 6) % 24 * crate::sim::TICKS_PER_HOUR;
446 *app.world_mut().resource_mut::<SimTick>() = SimTick(ticks);
447 }
448
449 fn structures(app: &mut App) -> Vec<(UVec2, StructureKind)> {
450 let world = app.world_mut();
451 world
452 .query::<(&TilePos, &Structure)>()
453 .iter(world)
454 .map(|(pos, s)| (pos.0, s.kind))
455 .collect()
456 }
457
458 fn put_campfire(app: &mut App, tile: UVec2) {
459 app.world_mut().spawn((
460 Structure {
461 kind: StructureKind::Campfire,
462 },
463 Fuel(StructureKind::Campfire.fuel_ticks().unwrap()),
464 TilePos(tile),
465 ));
466 }
467
468 #[test]
469 fn colonists_light_a_campfire_near_the_stockpile_when_night_falls() {
470 let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
471 put_wood(&mut app, UVec2::new(10, 10), 20);
472 set_hour(&mut app, 18);
473 spawn_colonist(&mut app, UVec2::new(2, 5));
474 run_ticks(&mut app, 60 * 20);
475 let built = structures(&mut app);
476 let campfire = built
477 .iter()
478 .find(|(_, kind)| *kind == StructureKind::Campfire)
479 .expect("a campfire");
480 assert!(campfire.0.as_vec2().distance(Vec2::new(11.0, 11.0)) <= HOME_RADIUS);
481 assert_eq!(wood_on_map(&mut app), 10);
482 }
483
484 #[test]
485 fn no_fires_are_built_during_the_day() {
486 let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
487 put_wood(&mut app, UVec2::new(10, 10), 20);
488 set_hour(&mut app, 10);
489 spawn_colonist(&mut app, UVec2::new(2, 5));
490 run_ticks(&mut app, 60 * 10);
491 assert!(
492 structures(&mut app)
493 .iter()
494 .all(|(_, kind)| kind.light().is_none())
495 );
496 }
497
498 #[test]
499 fn without_wood_nothing_can_be_built() {
500 let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
501 set_hour(&mut app, 22);
502 spawn_colonist(&mut app, UVec2::new(2, 5));
503 run_ticks(&mut app, 60 * 10);
504 assert!(
505 structures(&mut app)
506 .iter()
507 .all(|(_, kind)| kind.light().is_none())
508 );
509 }
510
511 #[test]
512 fn torches_light_stockpile_tiles_beyond_the_campfire() {
513 let mut app = hauling_app((IVec2::new(2, 10), IVec2::new(17, 10)));
514 put_wood(&mut app, UVec2::new(9, 10), 20);
515 put_campfire(&mut app, UVec2::new(9, 12));
516 set_hour(&mut app, 22);
517 spawn_colonist(&mut app, UVec2::new(9, 8));
518 run_ticks(&mut app, 60 * 15);
519 assert!(
520 structures(&mut app)
521 .iter()
522 .any(|(_, kind)| *kind == StructureKind::Torch)
523 );
524 }
525
526 // Light is what makes fires worth building: work in darkness goes at half speed.
527 #[test]
528 fn work_is_slower_in_the_dark() {
529 let tree = UVec2::new(8, 5);
530 let finish_ticks = |daylight: f32| {
531 let mut app = test_app(map_with_trees(&[tree]));
532 let size = app.world().resource::<WorldMap>().size;
533 app.insert_resource(LightMap::compute(size, daylight, []));
534 spawn_colonist(&mut app, UVec2::new(7, 5));
535 let mut ticks = 0;
536 while app
537 .world()
538 .resource::<WorldMap>()
539 .plant(tree.as_ivec2())
540 .is_some()
541 {
542 run_ticks(&mut app, 1);
543 ticks += 1;
544 }
545 ticks
546 };
547 let (lit, dark) = (finish_ticks(1.0), finish_ticks(0.0));
548 assert!(
549 dark > lit + WorkKind::Chop.work() / 2 - 10,
550 "lit {lit}, dark {dark}"
551 );
552 }
553
554 const NAP_TEST_REST: f32 = 0.25;
555
556 fn set_rest(app: &mut App, colonist: Entity, rest: f32) {
557 app.world_mut().get_mut::<Rest>(colonist).unwrap().0 = rest;
558 }
559
560 fn rest(app: &App, colonist: Entity) -> f32 {
561 app.world().get::<Rest>(colonist).unwrap().0
562 }
563
564 fn task(app: &App, colonist: Entity) -> Option<Task> {
565 job(app, colonist).map(|j| j.task)
566 }
567
568 #[test]
569 fn tired_colonists_nap_and_get_up_when_rested() {
570 let mut app = test_app(map_with_trees(&[]));
571 set_goals(&mut app, &[]);
572 set_hour(&mut app, 10);
573 let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
574 set_rest(&mut app, colonist, NAP_TEST_REST);
575 run_ticks(&mut app, 61);
576 assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
577 let mut ticks = 0;
578 while matches!(task(&app, colonist), Some(Task::Sleep { .. })) && ticks < 60 * 60 * 10 {
579 run_ticks(&mut app, 60);
580 ticks += 60;
581 }
582 assert!(!matches!(task(&app, colonist), Some(Task::Sleep { .. })));
583 assert!(rest(&app, colonist) > 0.95);
584 }
585
586 #[test]
587 fn colonists_go_to_bed_at_night_and_sleep_until_morning() {
588 let mut app = test_app(map_with_trees(&[]));
589 set_goals(&mut app, &[]);
590 set_hour(&mut app, 22);
591 let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
592 set_rest(&mut app, colonist, 0.8);
593 run_ticks(&mut app, 61);
594 assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
595 run_ticks(&mut app, 3 * crate::sim::TICKS_PER_HOUR as u32);
596 assert_eq!(rest(&app, colonist), 1.0);
597 assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
598 }
599
600 #[test]
601 fn rested_colonists_stay_up_during_the_day() {
602 let mut app = test_app(map_with_trees(&[]));
603 set_goals(&mut app, &[]);
604 set_hour(&mut app, 12);
605 let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
606 set_rest(&mut app, colonist, 0.8);
607 run_ticks(&mut app, 60 * 5);
608 assert!(!matches!(task(&app, colonist), Some(Task::Sleep { .. })));
609 }
610
611 #[test]
612 fn exhausted_colonists_drop_their_work_to_sleep() {
613 let tree = UVec2::new(15, 5);
614 let mut app = test_app(map_with_trees(&[tree]));
615 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
616 run_ticks(&mut app, 61);
617 assert!(matches!(task(&app, colonist), Some(Task::Work { .. })));
618 set_rest(&mut app, colonist, EXHAUSTED_REST / 2.0);
619 run_ticks(&mut app, 30);
620 assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
621 assert!(
622 app.world()
623 .resource::<Reservations>()
624 .is_free(Target::Site(tree))
625 );
626 }
627
628 #[test]
629 fn exhaustion_does_not_override_orders() {
630 let tree = UVec2::new(15, 5);
631 let mut app = test_app(map_with_trees(&[tree]));
632 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
633 app.world_mut().write_message(Order {
634 colonist,
635 tile: tree,
636 });
637 run_ticks(&mut app, 2);
638 set_rest(&mut app, colonist, EXHAUSTED_REST / 2.0);
639 run_ticks(&mut app, 30);
640 assert!(matches!(
641 job(&app, colonist),
642 Some(Job {
643 task: Task::Work { .. },
644 ordered: true
645 })
646 ));
647 }
648
649 fn beds(app: &mut App) -> usize {
650 structures(app)
651 .iter()
652 .filter(|(_, kind)| *kind == StructureKind::Bed)
653 .count()
654 }
655
656 #[test]
657 fn colonists_build_one_bed_each() {
658 let mut app = test_app(map_with_trees(&[]));
659 set_goals(&mut app, &[]);
660 let map = app.world().resource::<WorldMap>().clone();
661 app.world_mut()
662 .resource_mut::<Zones>()
663 .add_stockpile(&map, IVec2::new(2, 2), IVec2::new(5, 5));
664 put_wood(&mut app, UVec2::new(2, 2), 50);
665 put_wood(&mut app, UVec2::new(3, 2), 50);
666 spawn_colonist(&mut app, UVec2::new(10, 10));
667 spawn_colonist(&mut app, UVec2::new(11, 10));
668 run_ticks(&mut app, 60 * 40);
669 assert_eq!(beds(&mut app), 2);
670 assert_eq!(wood_on_map(&mut app), 100 - 2 * StructureKind::Bed.cost().1);
671 }
672
673 // A shelter needs a wall ring with walking space around the beds; beds hugging the stockpile
674 // would leave stored items where the walls must go and the room could never be planned.
675 #[test]
676 fn beds_keep_clear_of_the_stockpile() {
677 let mut app = test_app(map_with_trees(&[]));
678 set_goals(&mut app, &[]);
679 let map = app.world().resource::<WorldMap>().clone();
680 app.world_mut().resource_mut::<Zones>().add_stockpile(
681 &map,
682 IVec2::new(8, 8),
683 IVec2::new(11, 11),
684 );
685 put_wood(&mut app, UVec2::new(8, 8), 50);
686 spawn_colonist(&mut app, UVec2::new(10, 4));
687 run_ticks(&mut app, 60 * 20);
688 let built = structures(&mut app);
689 let bed = built
690 .iter()
691 .find(|(_, kind)| *kind == StructureKind::Bed)
692 .expect("a bed");
693 let zones = app.world().resource::<Zones>();
694 assert!(zones.stockpile_tiles().all(|s| {
695 (s.as_ivec2() - bed.0.as_ivec2()).abs().max_element() >= BED_STOCKPILE_CLEARANCE
696 }));
697 }
698
699 #[test]
700 fn colonists_sleep_in_their_beds() {
701 let mut app = test_app(map_with_trees(&[]));
702 set_goals(&mut app, &[]);
703 put_beds(&mut app, 1);
704 set_hour(&mut app, 22);
705 let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
706 set_rest(&mut app, colonist, 0.5);
707 run_ticks(&mut app, 60 * 10);
708 assert_eq!(
709 app.world().get::<TilePos>(colonist).unwrap().0,
710 UVec2::new(19, 19)
711 );
712 assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
713 }
714
715 fn put_items(app: &mut App, tile: UVec2, kind: ItemKind, count: u32) {
716 app.world_mut().spawn((Item { kind, count }, TilePos(tile)));
717 }
718
719 #[test]
720 fn blueprints_are_built_with_the_right_material() {
721 let mut app = test_app(map_with_trees(&[]));
722 set_goals(&mut app, &[]);
723 put_beds(&mut app, 1);
724 put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 10);
725 let wall = UVec2::new(8, 8);
726 app.world_mut()
727 .resource_mut::<Blueprints>()
728 .0
729 .insert(wall, BuildKind::Structure(StructureKind::Wall));
730 spawn_colonist(&mut app, UVec2::new(5, 5));
731 run_ticks(&mut app, 60 * 10);
732 assert!(app.world().resource::<WorldMap>().is_wall(wall.as_ivec2()));
733 assert!(app.world().resource::<Blueprints>().0.is_empty());
734 assert_eq!(items_of(&mut app, ItemKind::Stone), 8);
735 }
736
737 #[test]
738 fn colonists_build_a_roofed_shelter_around_their_beds() {
739 let mut app = test_app(map_with_trees(&[]));
740 app.add_systems(Update, crate::building::plan_shelter.before(choose_jobs));
741 set_goals(&mut app, &[]);
742 for x in [10, 11] {
743 app.world_mut().spawn((
744 Structure {
745 kind: StructureKind::Bed,
746 },
747 TilePos(UVec2::new(x, 10)),
748 ));
749 }
750 put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 50);
751 put_items(&mut app, UVec2::new(3, 2), ItemKind::Wood, 50);
752 spawn_colonist(&mut app, UVec2::new(4, 4));
753 spawn_colonist(&mut app, UVec2::new(5, 4));
754 run_ticks(&mut app, 60 * 150);
755 let map = app.world().resource::<WorldMap>();
756 assert!(map.is_roofed(IVec2::new(10, 10)) && map.is_roofed(IVec2::new(11, 10)));
757 let walls = map
758 .positions()
759 .filter(|p| map.is_wall(p.as_ivec2()))
760 .count();
761 assert_eq!(walls, 17);
762 assert!(
763 structures(&mut app)
764 .iter()
765 .any(|(_, kind)| *kind == StructureKind::Door)
766 );
767 }
768
769 #[test]
770 fn colonists_chop_from_a_neighbouring_tile() {
771 let tree = UVec2::new(8, 5);
772 let mut app = test_app(map_with_trees(&[tree]));
773 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
774 run_ticks(&mut app, 60 * 5);
775 let tile = app.world().get::<TilePos>(colonist).unwrap().0;
776 assert!(is_adjacent(tile.as_ivec2(), tree.as_ivec2()));
777 assert_eq!(
778 app.world().get::<SimPos>(colonist).unwrap().0,
779 WorldMap::tile_center(tile)
780 );
781 }
782
783 fn set_fed(app: &mut App, colonist: Entity, fed: f32) {
784 app.world_mut().get_mut::<Hunger>(colonist).unwrap().0 = fed;
785 }
786
787 fn fed(app: &App, colonist: Entity) -> f32 {
788 app.world().get::<Hunger>(colonist).unwrap().0
789 }
790
791 #[test]
792 fn hungry_colonists_eat_until_full() {
793 let mut app = test_app(map_with_trees(&[]));
794 set_goals(&mut app, &[]);
795 put_items(&mut app, UVec2::new(8, 5), ItemKind::Berries, 20);
796 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
797 set_fed(&mut app, colonist, 0.3);
798 run_ticks(&mut app, 60 * 6);
799 assert!(fed(&app, colonist) > 0.95);
800 assert_eq!(items_of(&mut app, ItemKind::Berries), 13);
801 }
802
803 #[test]
804 fn colonists_are_not_peckish_when_fed() {
805 let mut app = test_app(map_with_trees(&[]));
806 set_goals(&mut app, &[]);
807 put_items(&mut app, UVec2::new(8, 5), ItemKind::Berries, 20);
808 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
809 set_fed(&mut app, colonist, 0.8);
810 run_ticks(&mut app, 60 * 6);
811 assert_eq!(items_of(&mut app, ItemKind::Berries), 20);
812 }
813
814 #[test]
815 fn starving_colonists_drop_their_work_to_eat() {
816 let tree = UVec2::new(15, 5);
817 let mut app = test_app(map_with_trees(&[tree]));
818 put_items(&mut app, UVec2::new(2, 8), ItemKind::Meat, 20);
819 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
820 run_ticks(&mut app, 61);
821 assert!(task(&app, colonist).is_some_and(|t| !matches!(t, Task::Eat { .. })));
822 set_fed(&mut app, colonist, STARVING / 2.0);
823 run_ticks(&mut app, 30);
824 assert!(matches!(task(&app, colonist), Some(Task::Eat { .. })));
825 }
826
827 #[test]
828 fn colonists_forage_ripe_berries_when_the_colony_wants_them() {
829 let mut map = map_with_trees(&[]);
830 let ripe = UVec2::new(8, 5);
831 let unripe = UVec2::new(4, 5);
832 for (bush, growth) in [(ripe, 1.0), (unripe, 0.5)] {
833 map.set_plant(bush, Some(Plant::BerryBush));
834 map.set_growth(bush, growth);
835 }
836 let mut app = test_app(map);
837 set_goals(&mut app, &[(ItemKind::Berries, 30)]);
838 spawn_colonist(&mut app, UVec2::new(2, 5));
839 run_ticks(&mut app, 60 * 10);
840 assert_eq!(items_of(&mut app, ItemKind::Berries), 8);
841 let map = app.world().resource::<WorldMap>();
842 assert_eq!(map.plant(ripe.as_ivec2()), Some(Plant::BerryBush));
843 assert_eq!(map.growth(ripe.as_ivec2()), 0.0);
844 assert_eq!(map.growth(unripe.as_ivec2()), 0.5);
845 }
846
847 fn put_animal(app: &mut App, kind: AnimalKind, tile: UVec2) -> Entity {
848 app.world_mut()
849 .spawn((
850 Animal { kind },
851 TilePos(tile),
852 sim_pos(WorldMap::tile_center(tile)),
853 ))
854 .id()
855 }
856
857 #[test]
858 fn colonists_hunt_when_the_colony_wants_meat() {
859 let mut app = test_app(map_with_trees(&[]));
860 app.add_systems(Update, crate::animals::wander.before(choose_jobs));
861 set_goals(&mut app, &[(ItemKind::Meat, 20)]);
862 let deer = put_animal(&mut app, AnimalKind::Deer, UVec2::new(15, 15));
863 spawn_colonist(&mut app, UVec2::new(2, 2));
864 run_ticks(&mut app, 60 * 15);
865 assert!(app.world().get_entity(deer).is_err());
866 assert_eq!(items_of(&mut app, ItemKind::Meat), AnimalKind::Deer.meat());
867 }
868
869 #[test]
870 fn animals_are_left_alone_when_there_is_enough_meat() {
871 let mut app = test_app(map_with_trees(&[]));
872 set_goals(&mut app, &[]);
873 let rabbit = put_animal(&mut app, AnimalKind::Rabbit, UVec2::new(10, 10));
874 spawn_colonist(&mut app, UVec2::new(2, 2));
875 run_ticks(&mut app, 60 * 10);
876 assert!(app.world().get_entity(rabbit).is_ok());
877 }
878
879 #[test]
880 fn right_clicking_an_animal_orders_a_hunt() {
881 let mut app = test_app(map_with_trees(&[]));
882 set_goals(&mut app, &[]);
883 let rabbit = put_animal(&mut app, AnimalKind::Rabbit, UVec2::new(10, 10));
884 let colonist = spawn_colonist(&mut app, UVec2::new(2, 2));
885 app.world_mut().write_message(Order {
886 colonist,
887 tile: UVec2::new(10, 10),
888 });
889 run_ticks(&mut app, 60 * 10);
890 assert!(app.world().get_entity(rabbit).is_err());
891 assert_eq!(
892 items_of(&mut app, ItemKind::Meat),
893 AnimalKind::Rabbit.meat()
894 );
895 }
896
897 fn farm_app(tiles: &[UVec2]) -> App {
898 let mut map = map_with_trees(&[]);
899 for &t in tiles {
900 map.set(t, Terrain::Soil);
901 }
902 let mut app = test_app(map);
903 set_goals(&mut app, &[]);
904 put_beds(&mut app, 1);
905 let map = app.world().resource::<WorldMap>().clone();
906 app.world_mut()
907 .resource_mut::<Zones>()
908 .add_farm_tiles(&map, tiles.iter().copied());
909 app
910 }
911
912 #[test]
913 fn colonists_sow_empty_farm_tiles() {
914 let farm = [UVec2::new(8, 8), UVec2::new(9, 8)];
915 let mut app = farm_app(&farm);
916 spawn_colonist(&mut app, UVec2::new(2, 2));
917 run_ticks(&mut app, 60 * 10);
918 let map = app.world().resource::<WorldMap>();
919 assert!(
920 farm.iter()
921 .all(|t| map.plant(t.as_ivec2()) == Some(Plant::Crop))
922 );
923 }
924
925 #[test]
926 fn ripe_crops_are_harvested_into_vegetables_and_resown() {
927 let farm = [UVec2::new(8, 8)];
928 let mut app = farm_app(&farm);
929 set_goals(&mut app, &[(ItemKind::Vegetables, 30)]);
930 {
931 let mut map = app.world_mut().resource_mut::<WorldMap>();
932 map.set_plant(farm[0], Some(Plant::Crop));
933 map.set_growth(farm[0], 1.0);
934 }
935 spawn_colonist(&mut app, UVec2::new(2, 2));
936 run_ticks(&mut app, 60 * 15);
937 assert_eq!(items_of(&mut app, ItemKind::Vegetables), 10);
938 let map = app.world().resource::<WorldMap>();
939 assert_eq!(map.plant(farm[0].as_ivec2()), Some(Plant::Crop));
940 assert!(map.growth(farm[0].as_ivec2()) < 0.1);
941 }
942
943 #[test]
944 fn colonists_cook_raw_food_into_meals_at_a_campfire() {
945 let mut app = test_app(map_with_trees(&[]));
946 set_goals(&mut app, &[(ItemKind::Meal, 2)]);
947 put_beds(&mut app, 1);
948 put_campfire(&mut app, UVec2::new(10, 10));
949 put_items(&mut app, UVec2::new(5, 5), ItemKind::Meat, 12);
950 spawn_colonist(&mut app, UVec2::new(2, 2));
951 run_ticks(&mut app, 60 * 20);
952 assert_eq!(items_of(&mut app, ItemKind::Meal), 2);
953 assert_eq!(items_of(&mut app, ItemKind::Meat), 0);
954 }
955
956 #[test]
957 fn colonists_light_a_fire_to_cook_on() {
958 let mut app = test_app(map_with_trees(&[]));
959 set_goals(&mut app, &[(ItemKind::Meal, 1)]);
960 put_beds(&mut app, 1);
961 put_items(&mut app, UVec2::new(5, 5), ItemKind::Berries, 6);
962 put_items(&mut app, UVec2::new(6, 5), ItemKind::Wood, 10);
963 spawn_colonist(&mut app, UVec2::new(2, 2));
964 run_ticks(&mut app, 60 * 25);
965 assert!(
966 structures(&mut app)
967 .iter()
968 .any(|(_, kind)| *kind == StructureKind::Campfire)
969 );
970 assert_eq!(items_of(&mut app, ItemKind::Meal), 1);
971 }
972
973 #[test]
974 fn no_cooking_without_demand_for_meals() {
975 let mut app = test_app(map_with_trees(&[]));
976 set_goals(&mut app, &[]);
977 put_beds(&mut app, 1);
978 put_campfire(&mut app, UVec2::new(10, 10));
979 put_items(&mut app, UVec2::new(5, 5), ItemKind::Meat, 12);
980 spawn_colonist(&mut app, UVec2::new(2, 2));
981 run_ticks(&mut app, 60 * 10);
982 assert_eq!(items_of(&mut app, ItemKind::Meal), 0);
983 }
984
985 // Chopping and hauling leave material in many small piles; a builder gathers from several
986 // piles instead of waiting for one big enough to cover the whole cost.
987 #[test]
988 fn builders_gather_material_from_several_piles() {
989 let mut app = test_app(map_with_trees(&[]));
990 set_goals(&mut app, &[]);
991 put_wood(&mut app, UVec2::new(4, 4), 10);
992 put_wood(&mut app, UVec2::new(15, 4), 10);
993 spawn_colonist(&mut app, UVec2::new(2, 2));
994 run_ticks(&mut app, 60 * 30);
995 assert_eq!(beds(&mut app), 1);
996 assert_eq!(wood_on_map(&mut app), 20 - StructureKind::Bed.cost().1);
997 }
998
999 #[test]
1000 fn colonists_pave_planned_paths_with_stone() {
1001 let mut app = test_app(map_with_trees(&[]));
1002 set_goals(&mut app, &[]);
1003 put_beds(&mut app, 1);
1004 let tile = UVec2::new(8, 8);
1005 app.world_mut()
1006 .resource_mut::<WorldMap>()
1007 .set(tile, Terrain::Path);
1008 app.world_mut()
1009 .resource_mut::<Blueprints>()
1010 .0
1011 .insert(tile, BuildKind::Pave);
1012 put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 5);
1013 spawn_colonist(&mut app, UVec2::new(4, 4));
1014 run_ticks(&mut app, 60 * 10);
1015 assert_eq!(
1016 app.world().resource::<WorldMap>().get(tile.as_ivec2()),
1017 Some(Terrain::Paved)
1018 );
1019 assert_eq!(items_of(&mut app, ItemKind::Stone), 4);
1020 }
1021
1022 // The wall-builder must not trap a colonist who is just stepping onto the tile: that colonist's
1023 // TilePos is still the previous tile, so only checking TilePos would let the wall close on them.
1024 #[test]
1025 fn walls_wait_for_colonists_stepping_onto_the_tile() {
1026 let mut app = test_app(map_with_trees(&[]));
1027 set_goals(&mut app, &[]);
1028 put_beds(&mut app, 2);
1029 put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 10);
1030 let wall = UVec2::new(8, 8);
1031 app.world_mut()
1032 .resource_mut::<Blueprints>()
1033 .0
1034 .insert(wall, BuildKind::Structure(StructureKind::Wall));
1035 let colonists = [
1036 spawn_colonist(&mut app, UVec2::new(4, 4)),
1037 spawn_colonist(&mut app, UVec2::new(8, 10)),
1038 ];
1039 let mut sent = false;
1040 for _ in 0..60 * 20 {
1041 let nearly_built = |c| {
1042 matches!(
1043 task(&app, c),
1044 Some(Task::Build { progress, kind, .. }) if progress + 4 >= kind.work()
1045 )
1046 };
1047 if !sent && let Some(&builder) = colonists.iter().find(|&&c| nearly_built(c)) {
1048 let walker = *colonists.iter().find(|&&c| c != builder).unwrap();
1049 let from = app.world().get::<TilePos>(walker).unwrap().0;
1050 let route: VecDeque<UVec2> = find_path(app.world().resource::<WorldMap>(), from, wall)
1051 .unwrap()
1052 .into();
1053 app.world_mut().get_mut::<Path>(walker).unwrap().0 = route;
1054 sent = true;
1055 }
1056 run_ticks(&mut app, 1);
1057 let map = app.world().resource::<WorldMap>();
1058 for &c in &colonists {
1059 let tile = app.world().get::<TilePos>(c).unwrap().0;
1060 assert!(!map.is_wall(tile.as_ivec2()), "colonist trapped in a wall");
1061 }
1062 }
1063 assert!(sent);
1064 assert!(app.world().resource::<WorldMap>().is_wall(wall.as_ivec2()));
1065 }