pong_cli/
pong.rs

1use std::panic;
2
3use crossterm::AlternateScreen;
4use specs::{Builder, Dispatcher, DispatcherBuilder, World, WorldExt};
5
6use crate::components::prelude::*;
7use crate::geo::prelude::*;
8use crate::helpers::*;
9use crate::resources::prelude::*;
10use crate::settings::prelude::*;
11
12pub fn run() {
13    use std::thread::sleep;
14    use std::time::Duration;
15
16    panic::set_hook(Box::new(on_panic));
17
18    let final_scores;
19
20    // World is created in scope, so all it's resources are dropped when game is stopped.
21    // This prevents some terminal stdout issues.
22    {
23        let (mut world, mut dispatcher) = setup();
24
25        world.insert(Running(true));
26
27        let sleep_duration = Duration::from_millis(
28            world.read_resource::<Settings>().update_delay_ms,
29        );
30
31        while world.read_resource::<Running>().0 {
32            dispatcher.dispatch(&mut world);
33            world.maintain();
34            flush_stdout();
35            sleep(sleep_duration);
36        }
37
38        final_scores = (&*world.read_resource::<Scores>()).clone();
39        cleanup(Some(&world));
40    }
41
42    print_scores(&final_scores);
43}
44
45fn print_scores(scores: &Scores) {
46    #[cfg(feature = "style")]
47    use crossterm::{style, Attribute, Color};
48
49    const PADDING: &str = "  ";
50
51    #[cfg(feature = "style")]
52    let header_msg = style("Final Scores")
53        .with(Color::Blue)
54        .attr(Attribute::Bold)
55        .attr(Attribute::Underlined);
56
57    #[cfg(not(feature = "style"))]
58    let header_msg = "Final Scores";
59
60    println!(
61        "{}\n{}",
62        header_msg,
63        format!("{}", scores).replace("\n", format!("{}\n", PADDING).as_str())
64    );
65}
66
67fn on_panic(panic_info: &panic::PanicInfo) {
68    cleanup(None);
69    eprintln!("{:#}", panic_info);
70}
71
72fn cleanup(world: Option<&World>) {
73    if let Some(world) = world {
74        world.read_resource::<TerminalCursor>().show().unwrap();
75        world.read_resource::<AlternateScreen>().to_main().unwrap();
76    } else {
77        use crossterm::{execute, LeaveAlternateScreen};
78        use std::io::{stdout, Write};
79
80        TerminalCursor::new().show().unwrap();
81        execute!(stdout(), LeaveAlternateScreen).unwrap();
82    }
83}
84
85fn setup<'a, 'b>() -> (World, Dispatcher<'a, 'b>) {
86    const RAW_MODE: bool = true;
87
88    let mut world = World::new();
89    let dispatcher = new_dispatcher();
90
91    // Register components
92    world.register::<Paddle>();
93    world.register::<Position>();
94    world.register::<Size>();
95    world.register::<Drawable>();
96    world.register::<Velocity>();
97    world.register::<Collider>();
98    world.register::<Collision>();
99    world.register::<PaddleAi>();
100    world.register::<Ball>();
101    world.register::<Confined>();
102
103    // Insert resources
104    let settings = load_settings();
105    let cursor = TerminalCursor::new();
106    cursor.hide().unwrap();
107    world.insert(Deltatime::default());
108    world.insert(InputManager::new(settings.bindings.clone()));
109    world.insert(AlternateScreen::to_alternate(RAW_MODE).unwrap());
110    world.insert(cursor);
111    world.insert(TerminalInput::new());
112    world.insert(Scores::from(&settings.chars.score));
113    world.insert(ShouldReset::default());
114    world.insert(ShouldResetBallSpawns::default());
115    world.insert(settings);
116
117    // Create entities
118    create_paddles(&mut world);
119    create_vertical_walls(&mut world);
120
121    (world, dispatcher)
122}
123
124fn new_dispatcher<'a, 'b>() -> Dispatcher<'a, 'b> {
125    use crate::systems::prelude::*;
126
127    DispatcherBuilder::new()
128        .with(DeltatimeSystem::default(), "deltatime_system", &[])
129        .with(InputSystem::default(), "input_system", &[])
130        .with(
131            ControlPaddlesSystem::default(),
132            "control_paddles_system",
133            &["input_system"],
134        )
135        .with(PaddleAiSystem::default(), "paddle_ai_system", &[
136            "input_system",
137        ])
138        .with(MovePaddlesSystem::default(), "move_paddles_system", &[
139            "control_paddles_system",
140            "paddle_ai_system",
141        ])
142        .with(MoveEntitiesSystem::default(), "move_entities_system", &[
143            "deltatime_system",
144            "move_paddles_system",
145        ])
146        .with(
147            ConfineEntitiesSystem::default(),
148            "confine_entities_system",
149            &["move_entities_system"],
150        )
151        .with(BallBounceSystem::default(), "ball_bounce_system", &[
152            "move_entities_system",
153        ])
154        .with(BallScoreSystem::default(), "ball_score_system", &[
155            "move_entities_system",
156            "ball_bounce_system",
157        ])
158        .with(ResetSystem::default(), "reset_system", &[
159            "ball_score_system",
160        ])
161        .with(DrawRoomSystem::default(), "draw_room_system", &[
162            "move_entities_system",
163            "confine_entities_system",
164        ])
165        .with(DrawEntitiesSystem::default(), "draw_entities_system", &[
166            "move_entities_system",
167            "draw_room_system",
168        ])
169        .with(DrawScoresSystem::default(), "draw_scores_system", &[
170            "ball_score_system",
171            "draw_room_system",
172            "draw_entities_system",
173        ])
174        .with(SpawnBallSystem::default(), "spawn_ball_system", &[])
175        .build()
176}
177
178fn create_paddles(world: &mut World) {
179    let settings = (*world.read_resource::<Settings>()).clone();
180
181    let paddle_x = 1.0 + settings.paddle.size.0 * 0.5;
182    let paddle_y = settings.room.height as f32 * 0.5;
183    let paddle_size = Size::new(settings.paddle.size.0, settings.paddle.size.1);
184    let paddle_char = &settings.chars.paddle;
185    let room_rect = Rect {
186        top:    1.0,
187        bottom: (settings.room.height - 1) as f32,
188        left:   1.0,
189        right:  (settings.room.width - 1) as f32,
190    };
191
192    let drawable: Drawable = paddle_char.into();
193
194    // Left paddle
195    let mut left_paddle = world
196        .create_entity()
197        .with(Paddle::new(Side::Left))
198        .with(drawable.clone())
199        .with(position_for_paddle(&settings, &Side::Left))
200        .with(paddle_size.clone())
201        .with(Velocity::default())
202        .with(Collision::new(CollisionType::Paddle(Side::Left)))
203        .with(Confined::new(room_rect.clone()));
204    // Left paddle AI
205    if settings.paddle.ai.left {
206        left_paddle = left_paddle.with(PaddleAi::default());
207    }
208    left_paddle.build();
209
210    // Right paddle
211    let mut right_paddle = world
212        .create_entity()
213        .with(Paddle::new(Side::Right))
214        .with(drawable)
215        .with(Position::new(
216            settings.room.width as f32 - paddle_x,
217            paddle_y,
218        ))
219        .with(paddle_size.clone())
220        .with(Velocity::default())
221        .with(Collision::new(CollisionType::Paddle(Side::Right)))
222        .with(Confined::new(room_rect));
223    // Right paddle AI
224    if settings.paddle.ai.right {
225        right_paddle = right_paddle.with(PaddleAi::default());
226    }
227    right_paddle.build();
228}
229
230fn create_vertical_walls(world: &mut World) {
231    const WALL_HEIGHT: f32 = 8.0;
232    const WALL_Y_PADDING: f32 = 1.0;
233
234    let settings = (*world.read_resource::<Settings>()).clone();
235    let room_size = (settings.room.width as f32, settings.room.height as f32);
236    let half_room_size = (room_size.0 * 0.5, room_size.1 * 0.5);
237    let size = (room_size.0, WALL_HEIGHT);
238    let half_size = (half_room_size.0, size.1 * 0.5);
239
240    // Top edge
241    world
242        .create_entity()
243        .with(Position::new(
244            half_room_size.0,
245            -half_size.1 + WALL_Y_PADDING,
246        ))
247        .with(Size::new(size.0, size.1))
248        .with(Collision::new(CollisionType::Wall(Side::Top)))
249        .build();
250
251    // Bottom edge
252    world
253        .create_entity()
254        .with(Position::new(
255            half_room_size.0,
256            room_size.1 + half_size.1 - WALL_Y_PADDING,
257        ))
258        .with(Size::new(size.0, size.1))
259        .with(Collision::new(CollisionType::Wall(Side::Bottom)))
260        .build();
261}