1use std::cmp::max;
2use std::collections::HashMap;
3use std::time::{Duration, Instant};
4
5use crossterm::event;
6use crossterm::event::{Event, KeyCode, MouseButton, MouseEventKind};
7use tui::backend::Backend;
8use tui::layout::{Constraint, Direction, Layout};
9use tui::style::Color;
10use tui::widgets::Paragraph;
11use tui::{Frame, Terminal};
12
13use sea_battle_backend::data::{Coordinates, CurrentGameMapStatus, CurrentGameStatus};
14use sea_battle_backend::human_player_ws::{ClientMessage, ServerMessage};
15use sea_battle_backend::utils::res_utils::Res;
16use sea_battle_backend::utils::time_utils::time;
17
18use crate::client::Client;
19use crate::consts::*;
20use crate::ui_screens::confirm_dialog_screen::confirm;
21use crate::ui_screens::popup_screen::{show_screen_too_small_popup, PopupScreen};
22use crate::ui_screens::set_boats_layout_screen::SetBoatsLayoutScreen;
23use crate::ui_screens::utils::{
24 centered_rect_size, centered_rect_size_horizontally, centered_text,
25};
26use crate::ui_screens::ScreenResult;
27use crate::ui_widgets::button_widget::ButtonWidget;
28use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget};
29
30type CoordinatesMapper = HashMap<Coordinates, Coordinates>;
31
32#[derive(Eq, PartialEq, Ord, PartialOrd)]
33enum GameStatus {
34 Connecting,
35 WaitingForAnotherPlayer,
36 OpponentConnected,
37 WaitingForOpponentBoatsConfig,
38 OpponentReady,
39 Starting,
40 MustFire,
41 OpponentMustFire,
42 WonGame,
43 LostGame,
44 RematchRequestedByOpponent,
45 RematchRequestedByPlayer,
46 RematchAccepted,
47 RematchRejected,
48 OpponentLeftGame,
49}
50
51impl GameStatus {
52 pub fn can_show_game_maps(&self) -> bool {
53 self > &GameStatus::Starting
54 }
55
56 pub fn status_text(&self) -> &str {
57 match self {
58 GameStatus::Connecting => "š Connecting...",
59 GameStatus::WaitingForAnotherPlayer => "š Waiting for another player...",
60 GameStatus::OpponentConnected => "ā
Opponent connected!",
61 GameStatus::WaitingForOpponentBoatsConfig => "š Waiting for ### boats configuration",
62 GameStatus::OpponentReady => "ā
### is ready!",
63 GameStatus::Starting => "š Game is starting...",
64 GameStatus::MustFire => "šØ You must fire!",
65 GameStatus::OpponentMustFire => "š£ ### must fire!",
66 GameStatus::WonGame => "š You win the game!",
67 GameStatus::LostGame => "šæ ### wins the game. You loose.",
68 GameStatus::RematchRequestedByOpponent => "ā Rematch requested by ###",
69 GameStatus::RematchRequestedByPlayer => "ā Rematch requested by you",
70 GameStatus::RematchAccepted => "ā
Rematch accepted!",
71 GameStatus::RematchRejected => "ā Rematch rejected!",
72 GameStatus::OpponentLeftGame => "ā Opponent left game!",
73 }
74 }
75}
76
77#[derive(Debug, Eq, PartialEq, Copy, Clone)]
78enum Buttons {
79 RequestRematch,
80 AcceptRematch,
81 RejectRematch,
82 QuitGame,
83}
84
85impl Buttons {
86 pub fn text(&self) -> &str {
87 match self {
88 Buttons::RequestRematch => "ā Request rematch",
89 Buttons::AcceptRematch => "ā
Accept rematch",
90 Buttons::RejectRematch => "ā Reject rematch",
91 Buttons::QuitGame => "ā Quit game",
92 }
93 }
94}
95
96pub struct GameScreen {
97 client: Client,
98 invite_code: Option<String>,
99 status: GameStatus,
100 opponent_name: Option<String>,
101 game_last_update: u64,
102 game: CurrentGameStatus,
103 curr_shoot_position: Coordinates,
104 last_opponent_fire_position: Coordinates,
105 curr_button: usize,
106}
107
108impl GameScreen {
109 pub fn new(client: Client) -> Self {
110 Self {
111 client,
112 invite_code: None,
113 status: GameStatus::Connecting,
114 opponent_name: None,
115 game_last_update: 0,
116 game: Default::default(),
117 curr_shoot_position: Coordinates::new(0, 0),
118 last_opponent_fire_position: Coordinates::invalid(),
119 curr_button: 0,
120 }
121 }
122
123 pub async fn show<B: Backend>(mut self, terminal: &mut Terminal<B>) -> Res<ScreenResult> {
124 let mut last_tick = Instant::now();
125
126 let mut coordinates_mapper = CoordinatesMapper::new();
127
128 loop {
129 if !self.visible_buttons().is_empty() {
130 self.curr_button %= self.visible_buttons().len();
131 }
132
133 terminal.draw(|f| coordinates_mapper = self.ui(f))?;
135
136 let timeout = TICK_RATE
137 .checked_sub(last_tick.elapsed())
138 .unwrap_or_else(|| Duration::from_secs(0));
139
140 if event::poll(timeout)? {
142 let event = event::read()?;
143
144 if let Event::Key(key) = &event {
146 let mut new_shoot_pos = self.curr_shoot_position;
147
148 match key.code {
149 KeyCode::Char('q') | KeyCode::Esc
151 if confirm(terminal, "Do you really want to leave game?") =>
152 {
153 self.client.close_connection().await;
154 return Ok(ScreenResult::Canceled);
155 }
156
157 KeyCode::Left if self.can_fire() => new_shoot_pos = new_shoot_pos.add_x(-1),
159 KeyCode::Right if self.can_fire() => new_shoot_pos = new_shoot_pos.add_x(1),
160 KeyCode::Up if self.can_fire() => new_shoot_pos = new_shoot_pos.add_y(-1),
161 KeyCode::Down if self.can_fire() => new_shoot_pos = new_shoot_pos.add_y(1),
162
163 KeyCode::Enter if self.can_fire() => {
165 if self.game.can_fire_at_location(self.curr_shoot_position) {
166 self.client
167 .send_message(&ClientMessage::Fire {
168 location: self.curr_shoot_position,
169 })
170 .await?;
171 }
172 }
173
174 KeyCode::Left if self.game_over() => {
176 self.curr_button += self.visible_buttons().len() - 1
177 }
178 KeyCode::Right if self.game_over() => self.curr_button += 1,
179 KeyCode::Tab if self.game_over() => self.curr_button += 1,
180
181 KeyCode::Enter if self.game_over() => match self.curr_button() {
183 Buttons::RequestRematch => {
184 self.client
185 .send_message(&ClientMessage::RequestRematch)
186 .await?;
187 self.status = GameStatus::RematchRequestedByPlayer;
188 }
189 Buttons::AcceptRematch => {
190 self.client
191 .send_message(&ClientMessage::AcceptRematch)
192 .await?;
193 self.status = GameStatus::RematchAccepted;
194 }
195 Buttons::RejectRematch => {
196 self.client
197 .send_message(&ClientMessage::RejectRematch)
198 .await?;
199 self.status = GameStatus::RematchRejected;
200 }
201 Buttons::QuitGame => {
202 self.client.close_connection().await;
203 return Ok(ScreenResult::Ok(()));
204 }
205 },
206
207 _ => {}
208 }
209
210 if new_shoot_pos.is_valid(&self.game.rules) {
211 self.curr_shoot_position = new_shoot_pos;
212 }
213 }
214
215 if let Event::Mouse(mouse) = event {
217 if mouse.kind == MouseEventKind::Up(MouseButton::Left) {
218 if let Some(c) =
219 coordinates_mapper.get(&Coordinates::new(mouse.column, mouse.row))
220 {
221 self.curr_shoot_position = *c;
222
223 if self.can_fire()
224 && self.game.can_fire_at_location(self.curr_shoot_position)
225 {
226 self.client
227 .send_message(&ClientMessage::Fire {
228 location: self.curr_shoot_position,
229 })
230 .await?;
231 }
232 }
233 }
234 }
235 }
236
237 while let Some(msg) = self.client.try_recv_next_message().await? {
239 match msg {
240 ServerMessage::SetInviteCode { code } => {
241 self.status = GameStatus::WaitingForAnotherPlayer;
242 self.invite_code = Some(code);
243 }
244
245 ServerMessage::InvalidInviteCode => {
246 PopupScreen::new("ā Invalid invite code!").show(terminal)?;
247 return Ok(ScreenResult::Ok(()));
248 }
249
250 ServerMessage::WaitingForAnotherPlayer => {
251 self.status = GameStatus::WaitingForAnotherPlayer;
252 }
253
254 ServerMessage::OpponentConnected => {
255 self.status = GameStatus::OpponentConnected;
256 }
257
258 ServerMessage::SetOpponentName { name } => self.opponent_name = Some(name),
259
260 ServerMessage::QueryBoatsLayout { rules } => {
261 match SetBoatsLayoutScreen::new(&rules)
262 .set_confirm_on_cancel(true)
263 .show(terminal)?
264 {
265 ScreenResult::Ok(layout) => {
266 self.client
267 .send_message(&ClientMessage::BoatsLayout { layout })
268 .await?
269 }
270 ScreenResult::Canceled => {
271 self.client.close_connection().await;
272 return Ok(ScreenResult::Canceled);
273 }
274 };
275 }
276
277 ServerMessage::RejectedBoatsLayout { .. } => {
278 PopupScreen::new("Server rejected boats layout!! (is your version of SeaBattle up to date?)")
279 .show(terminal)?;
280 }
281
282 ServerMessage::WaitingForOtherPlayerConfiguration => {
283 self.status = GameStatus::WaitingForOpponentBoatsConfig;
284 }
285
286 ServerMessage::OpponentReady => {
287 self.status = GameStatus::OpponentReady;
288 }
289
290 ServerMessage::GameStarting => {
291 self.status = GameStatus::Starting;
292 }
293
294 ServerMessage::OpponentMustFire { status } => {
295 self.status = GameStatus::OpponentMustFire;
296 self.game_last_update = time();
297 self.game = status;
298 }
299
300 ServerMessage::RequestFire { status } => {
301 self.status = GameStatus::MustFire;
302 self.game_last_update = time();
303 self.game = status;
304 }
305
306 ServerMessage::FireResult { .. } => { }
307
308 ServerMessage::OpponentFireResult { pos, .. } => {
309 self.last_opponent_fire_position = pos;
310 }
311
312 ServerMessage::LostGame { status } => {
313 self.game_last_update = time();
314 self.game = status;
315 self.status = GameStatus::LostGame;
316 }
317
318 ServerMessage::WonGame { status } => {
319 self.game_last_update = time();
320 self.game = status;
321 self.status = GameStatus::WonGame;
322 }
323
324 ServerMessage::OpponentRequestedRematch => {
325 self.status = GameStatus::RematchRequestedByOpponent;
326 }
327
328 ServerMessage::OpponentAcceptedRematch => {
329 self.status = GameStatus::RematchAccepted;
330 }
331
332 ServerMessage::OpponentRejectedRematch => {
333 self.status = GameStatus::RematchRejected;
334 }
335
336 ServerMessage::OpponentLeftGame => {
337 self.status = GameStatus::OpponentLeftGame;
338 }
339
340 ServerMessage::OpponentReplacedByBot => {
341 PopupScreen::new("Opponent was replaced by a bot.").show(terminal)?;
342 }
343 }
344 }
345
346 if last_tick.elapsed() >= TICK_RATE {
347 last_tick = Instant::now();
348 }
349 }
350 }
351
352 fn can_fire(&self) -> bool {
353 matches!(self.status, GameStatus::MustFire)
354 }
355
356 fn game_over(&self) -> bool {
357 self.game.is_game_over()
358 }
359
360 fn visible_buttons(&self) -> Vec<Buttons> {
361 let mut buttons = vec![];
362 if self.game_over() && self.status != GameStatus::RematchAccepted {
363 if self.status == GameStatus::RematchRequestedByOpponent {
365 buttons.push(Buttons::AcceptRematch);
366 buttons.push(Buttons::RejectRematch);
367 } else if self.status != GameStatus::OpponentLeftGame
368 && self.status != GameStatus::RematchRejected
369 && self.status != GameStatus::RematchRequestedByPlayer
370 {
371 buttons.push(Buttons::RequestRematch);
372 }
373
374 buttons.push(Buttons::QuitGame);
375 }
376
377 buttons
378 }
379
380 fn opponent_name(&self) -> &str {
381 self.opponent_name.as_deref().unwrap_or("opponent")
382 }
383
384 fn curr_button(&self) -> Buttons {
385 self.visible_buttons()[self.curr_button]
386 }
387
388 fn player_map(&self, map: &CurrentGameMapStatus, opponent_map: bool) -> GameMapWidget {
389 let mut map_widget = GameMapWidget::new(&self.game.rules).set_default_empty_char(' ');
390
391 if opponent_map {
393 map_widget = map_widget.add_colored_cells(ColoredCells {
394 color: match (
395 self.game.can_fire_at_location(self.curr_shoot_position),
396 self.game
397 .opponent_map
398 .successful_strikes
399 .contains(&self.curr_shoot_position),
400 ) {
401 (true, _) => Color::Green,
402 (false, false) => Color::LightYellow,
403 (false, true) => Color::LightRed,
404 },
405 cells: vec![self.curr_shoot_position],
406 });
407 } else {
408 map_widget = map_widget.add_colored_cells(ColoredCells {
409 color: Color::Green,
410 cells: vec![self.last_opponent_fire_position],
411 });
412 }
413
414 for b in &map.sunk_boats {
416 for c in b.all_coordinates() {
417 map_widget =
418 map_widget.set_char(c, b.len.to_string().chars().next().unwrap_or('9'));
419 }
420 }
421 let sunk_boats = ColoredCells {
422 color: Color::LightRed,
423 cells: map
424 .sunk_boats
425 .iter()
426 .flat_map(|b| b.all_coordinates())
427 .collect::<Vec<_>>(),
428 };
429
430 for b in &map.successful_strikes {
432 map_widget = map_widget.set_char_no_overwrite(*b, 'T');
433 }
434 let touched_areas = ColoredCells {
435 color: Color::Red,
436 cells: map.successful_strikes.clone(),
437 };
438
439 for b in &map.failed_strikes {
441 map_widget = map_widget.set_char_no_overwrite(*b, '.');
442 }
443 let failed_strikes = ColoredCells {
444 color: Color::Black,
445 cells: map.failed_strikes.clone(),
446 };
447
448 for b in &map.boats.0 {
450 for c in b.all_coordinates() {
451 map_widget = map_widget.set_char_no_overwrite(c, 'B');
452 }
453 }
454 let boats = ColoredCells {
455 color: Color::Blue,
456 cells: map
457 .boats
458 .0
459 .iter()
460 .flat_map(|b| b.all_coordinates())
461 .collect::<Vec<_>>(),
462 };
463
464 map_widget
465 .add_colored_cells(sunk_boats)
466 .add_colored_cells(touched_areas)
467 .add_colored_cells(failed_strikes)
468 .add_colored_cells(boats)
469 }
470
471 fn ui<B: Backend>(&mut self, f: &mut Frame<B>) -> CoordinatesMapper {
472 let mut status_text = self
473 .status
474 .status_text()
475 .replace("###", self.opponent_name());
476
477 if !self.status.can_show_game_maps() {
479 if self.status == GameStatus::WaitingForAnotherPlayer {
480 if let Some(code) = &self.invite_code {
481 status_text.push_str(&format!("\n\nš« Invite code: {}", code));
482 }
483 }
484
485 PopupScreen::new(&status_text).show_in_frame(f);
486 return HashMap::default();
487 }
488
489 let mut timeout_str = String::new();
491 if self.status == GameStatus::MustFire || self.status == GameStatus::OpponentMustFire {
492 if let Some(remaining) = self.game.remaining_time_for_strike {
493 let timeout = self.game_last_update + remaining;
494 if time() < timeout {
495 timeout_str = format!(" {} seconds left", timeout - time());
496 }
497 }
498 }
499
500 let player_map = self
502 .player_map(&self.game.your_map, false)
503 .set_title("YOUR map");
504
505 let mut coordinates_mapper = HashMap::new();
506 let mut opponent_map = self
507 .player_map(&self.game.opponent_map, true)
508 .set_title(self.opponent_name())
509 .set_yield_func(|c, r| {
510 for i in 0..r.width {
511 for j in 0..r.height {
512 coordinates_mapper.insert(Coordinates::new(r.x + i, r.y + j), c);
513 }
514 }
515 });
516
517 if self.can_fire() {
518 opponent_map = opponent_map
519 .set_legend("Use arrows + Enter\nor click on the place\nwhere you want\nto shoot");
520 }
521
522 let buttons = self
524 .visible_buttons()
525 .iter()
526 .map(|b| ButtonWidget::new(b.text(), self.curr_button() == *b))
527 .collect::<Vec<_>>();
528
529 let player_map_size = player_map.estimated_size();
531 let opponent_map_size = opponent_map.estimated_size();
532 let both_maps_width = player_map_size.0 + opponent_map_size.0 + 3;
533 let show_both_maps = both_maps_width <= f.size().width;
534
535 let maps_height = max(player_map_size.1, opponent_map_size.1);
536 let maps_width = match show_both_maps {
537 true => both_maps_width,
538 false => max(player_map_size.0, opponent_map_size.0),
539 };
540
541 let buttons_width = buttons.iter().fold(0, |a, b| a + b.estimated_size().0 + 4);
542
543 let max_width = max(maps_width, status_text.len() as u16)
544 .max(buttons_width)
545 .max(timeout_str.len() as u16);
546 let total_height = 3 + 1 + maps_height + 3;
547
548 if max_width > f.size().width || total_height > f.size().height {
550 show_screen_too_small_popup(f);
551 return HashMap::default();
552 }
553
554 let chunks = Layout::default()
555 .direction(Direction::Vertical)
556 .constraints([
557 Constraint::Length(2),
558 Constraint::Length(2),
559 Constraint::Length(maps_height),
560 Constraint::Length(3),
561 ])
562 .split(centered_rect_size(max_width, total_height, &f.size()));
563
564 let paragraph = Paragraph::new(status_text.as_str());
566 f.render_widget(paragraph, centered_text(&status_text, &chunks[0]));
567
568 let paragraph = Paragraph::new(timeout_str.as_str());
570 f.render_widget(paragraph, centered_text(&timeout_str, &chunks[1]));
571
572 if show_both_maps {
574 let maps_chunks = Layout::default()
575 .direction(Direction::Horizontal)
576 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
577 .split(chunks[2]);
578
579 f.render_widget(
580 player_map,
581 centered_rect_size_horizontally(player_map_size.0, &maps_chunks[0]),
582 );
583 f.render_widget(
584 opponent_map,
585 centered_rect_size_horizontally(opponent_map_size.0, &maps_chunks[1]),
586 );
587 } else {
588 if self.can_fire() {
590 f.render_widget(opponent_map, chunks[2]);
591 } else {
592 f.render_widget(player_map, chunks[2]);
593 drop(opponent_map);
594 }
595 }
596
597 if !buttons.is_empty() {
599 let buttons_area = Layout::default()
600 .direction(Direction::Horizontal)
601 .constraints(
602 (0..buttons.len())
603 .map(|_| Constraint::Percentage(100 / buttons.len() as u16))
604 .collect::<Vec<_>>(),
605 )
606 .split(chunks[3]);
607
608 for (idx, b) in buttons.into_iter().enumerate() {
609 let target = centered_rect_size(
610 b.estimated_size().0,
611 b.estimated_size().1,
612 &buttons_area[idx],
613 );
614 f.render_widget(b, target);
615 }
616 }
617
618 coordinates_mapper
619 }
620}