Skip to content

Commit 599d67a

Browse files
committed
Improves Asciiquarium entity spawning and movement
Refactors Asciiquarium to deterministically spawn ships, sharks, and whales when none are present, using a tick-based system to schedule spawns. Entities now despawn when fully off-screen, and their respawn is scheduled for a later tick. Adjusts fish movement speed and bubble emission frequency for better pacing.
1 parent 579537f commit 599d67a

File tree

2 files changed

+98
-56
lines changed

2 files changed

+98
-56
lines changed

examples/egui_demo.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,13 @@ fn spawn_random_fish(state: &mut AquariumState, asset_count: usize) {
158158
let x = rng.gen_range(0..=max_x) as f32;
159159
let y = rng.gen_range(0..=max_y) as f32;
160160

161-
// Random velocity with minimum magnitude to avoid stationary fish
162-
let mut vx = rng.gen_range(-0.25_f32..=0.25_f32);
163-
let mut vy = rng.gen_range(-0.12_f32..=0.12_f32);
164-
if vx.abs() < 0.03 {
165-
vx = if vx.is_sign_negative() { -0.04 } else { 0.04 };
166-
}
167-
if vy.abs() < 0.012 {
168-
vy = if vy.is_sign_negative() { -0.015 } else { 0.015 };
161+
// Classic Asciiquarium pacing: horizontal speed 2.5..22.5 cps, minimal vertical drift
162+
let speed = rng.gen_range(2.5_f32..=22.5_f32);
163+
let dir = if rng.gen_bool(0.5) { -1.0 } else { 1.0 };
164+
let vx = dir * speed;
165+
let mut vy = rng.gen_range(-0.6_f32..=0.6_f32);
166+
if vy.abs() < 0.05 {
167+
vy = 0.0;
169168
}
170169

171170
state.fishes.push(FishInstance {

src/widgets/asciiquarium.rs

Lines changed: 91 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ pub struct AquariumEnvironment {
8686
pub sharks: Vec<Shark>,
8787
/// Underwater whales.
8888
pub whales: Vec<Whale>,
89+
/// Next eligible tick to spawn a ship/shark/whale when none present.
90+
pub next_ship_spawn: u64,
91+
pub next_shark_spawn: u64,
92+
pub next_whale_spawn: u64,
8993
}
9094

9195
impl Default for AquariumEnvironment {
@@ -97,6 +101,9 @@ impl Default for AquariumEnvironment {
97101
ships: Vec::new(),
98102
sharks: Vec::new(),
99103
whales: Vec::new(),
104+
next_ship_spawn: 0,
105+
next_shark_spawn: 0,
106+
next_whale_spawn: 0,
100107
}
101108
}
102109
}
@@ -320,45 +327,66 @@ fn ensure_environment_initialized(state: &mut AquariumState) {
320327
pub fn update_aquarium(state: &mut AquariumState, assets: &[FishArt]) {
321328
let (aw, ah) = (state.size.0 as f32, state.size.1 as f32);
322329
let dt: f32 = 0.033;
330+
let fish_speed_mult: f32 = 2.0;
323331

324332
// Ensure environment exists.
325333
ensure_environment_initialized(state);
326334

327335
// Integrate fish and handle bounce.
328336

329-
// Spawn default moving entities if none exist.
330-
if state.env.ships.is_empty() {
331-
// Start a ship just off the left edge moving right.
332-
let (sw, _) = measure_block(SHIP_R);
333-
state.env.ships.push(Ship {
334-
x: -(sw as f32),
335-
y: 0,
336-
vx: 6.0,
337-
});
337+
// Spawn entities deterministically when none present and past next spawn tick.
338+
if state.env.ships.is_empty() && state.tick >= state.env.next_ship_spawn {
339+
// Alternate direction by epoch (simple deterministic scheme).
340+
let right = (state.tick / 900) % 2 == 0;
341+
let (sw, _) = if right {
342+
measure_block(SHIP_R)
343+
} else {
344+
measure_block(SHIP_L)
345+
};
346+
let (x, vx) = if right {
347+
(-(sw as f32), 6.0)
348+
} else {
349+
(state.size.0 as f32 + sw as f32, -6.0)
350+
};
351+
state.env.ships.push(Ship { x, y: 0, vx });
338352
}
339-
if state.env.sharks.is_empty() {
340-
// Place shark below waterlines.
353+
if state.env.sharks.is_empty() && state.tick >= state.env.next_shark_spawn {
354+
// Place shark at a consistent depth under waterlines.
341355
let (_, sh) = measure_block(SHARK_R);
342356
let base = 9;
343357
let y = state.size.1.saturating_sub(sh + 3).max(base);
344-
state.env.sharks.push(Shark {
345-
x: -40.0,
346-
y,
347-
vx: 8.0,
348-
});
358+
let right = (state.tick / 1200) % 2 == 0;
359+
let (sw, _) = if right {
360+
measure_block(SHARK_R)
361+
} else {
362+
measure_block(SHARK_L)
363+
};
364+
let (x, vx) = if right {
365+
(-(sw as f32), 8.0)
366+
} else {
367+
(state.size.0 as f32 + sw as f32, -8.0)
368+
};
369+
state.env.sharks.push(Shark { x, y, vx });
349370
}
350-
if state.env.whales.is_empty() {
351-
// Place whale at mid-depth moving left.
371+
if state.env.whales.is_empty() && state.tick >= state.env.next_whale_spawn {
372+
// Mid-depth whale.
352373
let y = (state.size.1 / 3).max(6);
353-
state.env.whales.push(Whale {
354-
x: state.size.0 as f32 + 10.0,
355-
y,
356-
vx: -4.0,
357-
});
374+
let right = (state.tick / 1500) % 2 == 0;
375+
let (ww, _) = if right {
376+
measure_block(WHALE_R)
377+
} else {
378+
measure_block(WHALE_L)
379+
};
380+
let (x, vx) = if right {
381+
(-(ww as f32), 4.0)
382+
} else {
383+
(state.size.0 as f32 + ww as f32, -4.0)
384+
};
385+
state.env.whales.push(Whale { x, y, vx });
358386
}
359387
for fish in &mut state.fishes {
360-
fish.position.0 += fish.velocity.0 * dt;
361-
fish.position.1 += fish.velocity.1 * dt;
388+
fish.position.0 += fish.velocity.0 * dt * fish_speed_mult;
389+
fish.position.1 += fish.velocity.1 * dt * fish_speed_mult;
362390

363391
let (fw, fh) = assets
364392
.get(fish.fish_art_index)
@@ -385,7 +413,7 @@ pub fn update_aquarium(state: &mut AquariumState, assets: &[FishArt]) {
385413
// Occasionally emit bubbles from fish mouths, deterministically based on tick.
386414
// Emit every 24 ticks per fish to avoid randomness in the core crate.
387415
for fish in &state.fishes {
388-
if state.tick % 24 == 0 {
416+
if state.tick % 72 == 0 {
389417
let (fw, fh) = assets
390418
.get(fish.fish_art_index)
391419
.map(|a| (a.width as f32, a.height as f32))
@@ -415,55 +443,70 @@ pub fn update_aquarium(state: &mut AquariumState, assets: &[FishArt]) {
415443
}
416444
state.bubbles = kept;
417445

418-
// Move ships.
419-
for ship in &mut state.env.ships {
446+
// Move ships and despawn when fully off-screen. Schedule next spawn.
447+
let mut next_ships = Vec::with_capacity(state.env.ships.len());
448+
for mut ship in state.env.ships.drain(..) {
420449
ship.x += ship.vx * dt;
421450
let (sw, _) = if ship.vx >= 0.0 {
422451
measure_block(SHIP_R)
423452
} else {
424453
measure_block(SHIP_L)
425454
};
426-
if ship.vx >= 0.0 && ship.x > state.size.0 as f32 {
427-
ship.x = -(sw as f32);
428-
} else if ship.vx < 0.0 && ship.x + sw as f32 <= 0.0 {
429-
ship.x = state.size.0 as f32;
455+
let off_right = ship.x > state.size.0 as f32;
456+
let off_left = ship.x + sw as f32 <= 0.0;
457+
if off_right || off_left {
458+
// Next ship after ~20s
459+
state.env.next_ship_spawn = state.tick + 600;
460+
} else {
461+
next_ships.push(ship);
430462
}
431463
}
464+
state.env.ships = next_ships;
432465

433-
// Move sharks.
434-
for shark in &mut state.env.sharks {
466+
// Move sharks and despawn when fully off-screen. Schedule next spawn.
467+
let mut next_sharks = Vec::with_capacity(state.env.sharks.len());
468+
for mut shark in state.env.sharks.drain(..) {
435469
shark.x += shark.vx * dt;
436470
let (sw, _) = if shark.vx >= 0.0 {
437471
measure_block(SHARK_R)
438472
} else {
439473
measure_block(SHARK_L)
440474
};
441-
if shark.vx >= 0.0 && shark.x > state.size.0 as f32 {
442-
// Re-enter from left
443-
shark.x = -(sw as f32);
444-
} else if shark.vx < 0.0 && shark.x + sw as f32 <= 0.0 {
445-
// Re-enter from right
446-
shark.x = state.size.0 as f32;
475+
let off_right = shark.x > state.size.0 as f32;
476+
let off_left = shark.x + sw as f32 <= 0.0;
477+
if off_right || off_left {
478+
// Next shark after ~30s
479+
state.env.next_shark_spawn = state.tick + 900;
480+
} else {
481+
next_sharks.push(shark);
447482
}
448483
}
484+
state.env.sharks = next_sharks;
449485

450-
// Move whales.
451-
for whale in &mut state.env.whales {
486+
// Move whales and despawn when fully off-screen. Schedule next spawn.
487+
let mut next_whales = Vec::with_capacity(state.env.whales.len());
488+
for mut whale in state.env.whales.drain(..) {
452489
whale.x += whale.vx * dt;
453490
let (ww, _) = if whale.vx >= 0.0 {
454491
measure_block(WHALE_R)
455492
} else {
456493
measure_block(WHALE_L)
457494
};
458-
if whale.vx >= 0.0 && whale.x > state.size.0 as f32 {
459-
whale.x = -(ww as f32);
460-
} else if whale.vx < 0.0 && whale.x + ww as f32 <= 0.0 {
461-
whale.x = state.size.0 as f32;
495+
let off_right = whale.x > state.size.0 as f32;
496+
let off_left = whale.x + ww as f32 <= 0.0;
497+
if off_right || off_left {
498+
// Next whale after ~40s
499+
state.env.next_whale_spawn = state.tick + 1200;
500+
} else {
501+
next_whales.push(whale);
462502
}
463503
}
504+
state.env.whales = next_whales;
464505

465506
// Advance environment phases.
466-
state.env.water_phase = state.env.water_phase.wrapping_add(1);
507+
if state.tick % 4 == 0 {
508+
state.env.water_phase = state.env.water_phase.wrapping_add(1);
509+
}
467510
state.tick = state.tick.wrapping_add(1);
468511
}
469512

@@ -638,7 +681,7 @@ pub fn render_aquarium_to_string(state: &AquariumState, assets: &[FishArt]) -> S
638681
}
639682
}
640683
// Water spout above head (simple animation)
641-
let frame = (state.tick as usize / 4) % SPOUT_FRAMES.len();
684+
let frame = (state.tick as usize / 12) % SPOUT_FRAMES.len();
642685
let spout = SPOUT_FRAMES[frame];
643686
// Approximate blowhole position a bit right of whale x
644687
let spx = x0 + if whale.vx >= 0.0 { 8 } else { 3 };

0 commit comments

Comments
 (0)