falling_sand/
main.rs

1use std::io::stdout;
2use teng::components::Component;
3use teng::rendering::color::Color;
4use teng::rendering::render::{HalfBlockDisplayRender, Render};
5use teng::rendering::renderer::Renderer;
6use teng::util::planarvec::{Bounds, PlanarVec};
7use teng::{
8    install_panic_handler, terminal_cleanup, terminal_setup, DisplayInfo, Game, SetupInfo,
9    SharedState, UpdateInfo,
10};
11
12fn main() -> std::io::Result<()> {
13    terminal_setup()?;
14    install_panic_handler();
15
16    let mut game = Game::new(stdout());
17    game.install_recommended_components();
18    game.add_component(Box::new(FallingSimulationComponent::new()));
19    game.run()?;
20
21    terminal_cleanup()?;
22
23    Ok(())
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
27enum PieceKind {
28    Air,
29    Sand,
30    Water,
31}
32
33impl PieceKind {
34    fn density(&self) -> f64 {
35        match self {
36            PieceKind::Air => 0.0,
37            PieceKind::Sand => 2.0,
38            PieceKind::Water => 1.0,
39        }
40    }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
44struct Piece {
45    kind: PieceKind,
46}
47
48#[derive(Default)]
49struct FallingSimulationData {
50    secs_passed: f64,
51    total_pieces: usize,
52    world: PlanarVec<Piece>,
53    has_moved: PlanarVec<bool>,
54}
55
56impl FallingSimulationData {
57    fn new() -> Self {
58        let bounds = Bounds {
59            min_x: -100,
60            max_x: 100,
61            min_y: -100,
62            max_y: 100,
63        };
64
65        Self {
66            secs_passed: 0.0,
67            total_pieces: 0,
68            world: PlanarVec::new(
69                bounds,
70                Piece {
71                    kind: PieceKind::Air,
72                },
73            ),
74            has_moved: PlanarVec::new(bounds, false),
75        }
76    }
77
78    fn swap(&mut self, (x1, y1): (i64, i64), (x2, y2): (i64, i64)) {
79        let temp = self.world[(x1, y1)];
80        self.world[(x1, y1)] = self.world[(x2, y2)];
81        self.world[(x2, y2)] = temp;
82    }
83
84    fn sim_sand(&mut self, (x, y): (i64, i64)) {
85        let piece = self.world[(x, y)];
86
87        // check below
88        if let Some(&below) = self.world.get(x, y - 1) {
89            if below.kind.density() < piece.kind.density() {
90                self.swap((x, y), (x, y - 1));
91                self.has_moved[(x, y)] = true;
92                self.has_moved[(x, y - 1)] = true;
93                // moved, no more sim
94                return;
95            }
96        }
97        // check below and right
98        if let Some(&below_right) = self.world.get(x + 1, y - 1) {
99            if below_right.kind.density() < piece.kind.density() {
100                self.swap((x, y), (x + 1, y - 1));
101                self.has_moved[(x, y)] = true;
102                self.has_moved[(x + 1, y - 1)] = true;
103                // moved, no more sim
104                return;
105            }
106        }
107        // check below and left
108        if let Some(&below_left) = self.world.get(x - 1, y - 1) {
109            if below_left.kind.density() < piece.kind.density() {
110                self.swap((x, y), (x - 1, y - 1));
111                self.has_moved[(x, y)] = true;
112                self.has_moved[(x - 1, y - 1)] = true;
113                // moved, no more sim
114                return;
115            }
116        }
117    }
118
119    fn sim_water(&mut self, (x, y): (i64, i64)) {
120        let piece = self.world[(x, y)];
121
122        // check below
123        if let Some(&below) = self.world.get(x, y - 1) {
124            if below.kind.density() < piece.kind.density() {
125                self.swap((x, y), (x, y - 1));
126                self.has_moved[(x, y)] = true;
127                self.has_moved[(x, y - 1)] = true;
128                // moved, no more sim
129                return;
130            }
131        }
132        // check below and right
133        if let Some(&below_right) = self.world.get(x + 1, y - 1) {
134            if below_right.kind.density() < piece.kind.density() {
135                self.swap((x, y), (x + 1, y - 1));
136                self.has_moved[(x, y)] = true;
137                self.has_moved[(x + 1, y - 1)] = true;
138                // moved, no more sim
139                return;
140            }
141        }
142        // check below and left
143        if let Some(&below_left) = self.world.get(x - 1, y - 1) {
144            if below_left.kind.density() < piece.kind.density() {
145                self.swap((x, y), (x - 1, y - 1));
146                self.has_moved[(x, y)] = true;
147                self.has_moved[(x - 1, y - 1)] = true;
148                // moved, no more sim
149                return;
150            }
151        }
152        // check right
153        if let Some(&right) = self.world.get(x + 1, y) {
154            // note: we are not checking densities anymore, since this is on the horizontal axis.
155            if right.kind == PieceKind::Air {
156                self.swap((x, y), (x + 1, y));
157                self.has_moved[(x, y)] = true;
158                self.has_moved[(x + 1, y)] = true;
159                // moved, no more sim
160                return;
161            }
162        }
163        // check left
164        if let Some(&left) = self.world.get(x - 1, y) {
165            // note: we are not checking densities anymore, since this is on the horizontal axis.
166            if left.kind == PieceKind::Air {
167                self.swap((x, y), (x - 1, y));
168                self.has_moved[(x, y)] = true;
169                self.has_moved[(x - 1, y)] = true;
170                // moved, no more sim
171                return;
172            }
173        }
174    }
175
176    fn resize_discard(&mut self, width: usize, height: usize) {
177        let bounds = Bounds {
178            min_x: 0,
179            max_x: width as i64 - 1,
180            min_y: 0,
181            max_y: height as i64 - 1,
182        };
183
184        self.world = PlanarVec::new(
185            bounds,
186            Piece {
187                kind: PieceKind::Air,
188            },
189        );
190        self.has_moved = PlanarVec::new(bounds, false);
191    }
192}
193
194pub struct FallingSimulationComponent {
195    dt_budget: f64,
196    hb_display: HalfBlockDisplayRender,
197}
198
199impl FallingSimulationComponent {
200    const UPDATES_PER_SECOND: f64 = 100.0;
201    const UPDATE_INTERVAL: f64 = 1.0 / Self::UPDATES_PER_SECOND;
202
203    pub fn new() -> Self {
204        Self {
205            dt_budget: 0.0,
206            hb_display: HalfBlockDisplayRender::new(10, 10),
207        }
208    }
209
210    fn update_render(&mut self, data: &FallingSimulationData, display_info: &DisplayInfo) {
211        // TODO: add display here
212
213        for x in data.world.x_range() {
214            for y in data.world.y_range() {
215                let piece = data.world[(x, y)];
216                let color = match piece.kind {
217                    PieceKind::Air => Color::Transparent,
218                    PieceKind::Sand => Color::Rgb([255, 255, 0]),
219                    PieceKind::Water => Color::Rgb([0, 0, 255]),
220                };
221                let d_x = x;
222                let d_y = y;
223                let d_y = 2 * display_info.height() as i64 - d_y;
224                let d_y = d_y - 1;
225                self.hb_display.set_color(d_x as usize, d_y as usize, color);
226            }
227        }
228    }
229
230    fn update_simulation(&mut self, shared_state: &mut SharedState<FallingSimulationData>) {
231        let data = &mut shared_state.custom;
232        data.secs_passed += Self::UPDATE_INTERVAL;
233
234        // std::mem::swap(&mut data.world, &mut data.old_world);
235        // data.world.clear(Piece { kind: PieceKind::Air });
236
237        data.has_moved.clear(false);
238
239        data.total_pieces = 0;
240
241        // go over every piece (that is not air) and update it
242        for x in data.world.x_range() {
243            for y in data.world.y_range().rev() {
244                if data.has_moved[(x, y)] {
245                    continue;
246                }
247                let piece = data.world[(x, y)];
248                if piece.kind == PieceKind::Air {
249                    continue;
250                }
251
252                match piece.kind {
253                    PieceKind::Air => {
254                        // do nothing
255                    }
256                    PieceKind::Sand => {
257                        data.sim_sand((x, y));
258                    }
259                    PieceKind::Water => {
260                        data.sim_water((x, y));
261                    }
262                }
263                data.has_moved[(x, y)] = true;
264            }
265        }
266
267        for x in data.world.x_range() {
268            for y in data.world.y_range() {
269                let piece = data.world[(x, y)];
270                if piece.kind != PieceKind::Air {
271                    data.total_pieces += 1;
272                }
273            }
274        }
275
276        self.update_render(data, &shared_state.display_info);
277    }
278}
279
280impl Component<FallingSimulationData> for FallingSimulationComponent {
281    fn setup(
282        &mut self,
283        setup_info: &SetupInfo,
284        shared_state: &mut SharedState<FallingSimulationData>,
285    ) {
286        self.on_resize(
287            setup_info.display_info.width(),
288            setup_info.display_info.height(),
289            shared_state,
290        );
291    }
292
293    fn on_resize(
294        &mut self,
295        width: usize,
296        height: usize,
297        shared_state: &mut SharedState<FallingSimulationData>,
298    ) {
299        self.hb_display.resize_discard(width, height * 2);
300        let data = &mut shared_state.custom;
301        data.resize_discard(width, height * 2);
302    }
303
304    fn update(
305        &mut self,
306        update_info: UpdateInfo,
307        shared_state: &mut SharedState<FallingSimulationData>,
308    ) {
309        let dt = update_info.dt;
310        self.dt_budget += dt;
311
312        // add sand from mouse events
313        let data = &mut shared_state.custom;
314
315        if shared_state.mouse_info.left_mouse_down
316            || shared_state.mouse_info.right_mouse_down
317            || shared_state.mouse_info.middle_mouse_down
318        {
319            let (s_x, s_y) = shared_state.mouse_info.last_mouse_pos;
320
321            let x = s_x as i64;
322            // scale to two halfblocks per pixel and recenter to 0,0
323            let y = shared_state.display_info.height() as i64 - s_y as i64;
324            let y = 2 * y;
325            let y = y - 1;
326
327            if let Some(piece) = data.world.get_mut(x, y) {
328                let kind = if shared_state.mouse_info.left_mouse_down {
329                    PieceKind::Sand
330                } else if shared_state.mouse_info.right_mouse_down {
331                    PieceKind::Water
332                } else {
333                    PieceKind::Air
334                };
335                piece.kind = kind;
336            } else {
337                panic!("Mouse out of bounds: ({}, {})", x, y);
338            }
339        }
340
341        while self.dt_budget >= Self::UPDATE_INTERVAL {
342            self.update_simulation(shared_state);
343            self.dt_budget -= Self::UPDATE_INTERVAL;
344        }
345    }
346
347    fn render(
348        &self,
349        renderer: &mut dyn Renderer,
350        shared_state: &SharedState<FallingSimulationData>,
351        depth_base: i32,
352    ) {
353        let depth_base = i32::MAX - 99;
354        let data = &shared_state.custom;
355        format!("FallingSimulationComponent: {}s", data.secs_passed)
356            .render(renderer, 0, 0, depth_base);
357        format!("sands: [{}]", data.total_pieces).render(renderer, 0, 1, depth_base);
358
359        self.hb_display.render(renderer, 0, 0, depth_base);
360    }
361}