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