Can request & respond to rematch
This commit is contained in:
		@@ -21,6 +21,7 @@ use crate::ui_screens::popup_screen::PopupScreen;
 | 
				
			|||||||
use crate::ui_screens::set_boats_layout_screen::SetBoatsLayoutScreen;
 | 
					use crate::ui_screens::set_boats_layout_screen::SetBoatsLayoutScreen;
 | 
				
			||||||
use crate::ui_screens::utils::{centered_rect_size, centered_text};
 | 
					use crate::ui_screens::utils::{centered_rect_size, centered_text};
 | 
				
			||||||
use crate::ui_screens::ScreenResult;
 | 
					use crate::ui_screens::ScreenResult;
 | 
				
			||||||
 | 
					use crate::ui_widgets::button_widget::ButtonWidget;
 | 
				
			||||||
use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget};
 | 
					use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type CoordinatesMapper = HashMap<Coordinates, Coordinates>;
 | 
					type CoordinatesMapper = HashMap<Coordinates, Coordinates>;
 | 
				
			||||||
@@ -33,6 +34,13 @@ enum GameStatus {
 | 
				
			|||||||
    Starting,
 | 
					    Starting,
 | 
				
			||||||
    MustFire,
 | 
					    MustFire,
 | 
				
			||||||
    OpponentMustFire,
 | 
					    OpponentMustFire,
 | 
				
			||||||
 | 
					    WonGame,
 | 
				
			||||||
 | 
					    LostGame,
 | 
				
			||||||
 | 
					    RematchRequestedByOpponent,
 | 
				
			||||||
 | 
					    RematchRequestedByPlayer,
 | 
				
			||||||
 | 
					    RematchAccepted,
 | 
				
			||||||
 | 
					    RematchRejected,
 | 
				
			||||||
 | 
					    OpponentLeftGame,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl GameStatus {
 | 
					impl GameStatus {
 | 
				
			||||||
@@ -50,6 +58,32 @@ impl GameStatus {
 | 
				
			|||||||
            GameStatus::Starting => "Game is starting...",
 | 
					            GameStatus::Starting => "Game is starting...",
 | 
				
			||||||
            GameStatus::MustFire => "You must fire!",
 | 
					            GameStatus::MustFire => "You must fire!",
 | 
				
			||||||
            GameStatus::OpponentMustFire => "### must fire!",
 | 
					            GameStatus::OpponentMustFire => "### must fire!",
 | 
				
			||||||
 | 
					            GameStatus::WonGame => "You won the game!",
 | 
				
			||||||
 | 
					            GameStatus::LostGame => "### won the game. You loose.",
 | 
				
			||||||
 | 
					            GameStatus::RematchRequestedByOpponent => "Rematch requested by ###",
 | 
				
			||||||
 | 
					            GameStatus::RematchRequestedByPlayer => "Rematch requested by you",
 | 
				
			||||||
 | 
					            GameStatus::RematchAccepted => "Rematch accepted!",
 | 
				
			||||||
 | 
					            GameStatus::RematchRejected => "Rematch rejected!",
 | 
				
			||||||
 | 
					            GameStatus::OpponentLeftGame => "Opponent left game!",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Eq, PartialEq, Copy, Clone)]
 | 
				
			||||||
 | 
					enum Buttons {
 | 
				
			||||||
 | 
					    RequestRematch,
 | 
				
			||||||
 | 
					    AcceptRematch,
 | 
				
			||||||
 | 
					    RejectRematch,
 | 
				
			||||||
 | 
					    QuitGame,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Buttons {
 | 
				
			||||||
 | 
					    pub fn text(&self) -> &str {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Buttons::RequestRematch => "Request rematch",
 | 
				
			||||||
 | 
					            Buttons::AcceptRematch => "Accept rematch",
 | 
				
			||||||
 | 
					            Buttons::RejectRematch => "Reject rematch",
 | 
				
			||||||
 | 
					            Buttons::QuitGame => "Quit game",
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -61,6 +95,7 @@ pub struct GameScreen {
 | 
				
			|||||||
    game: CurrentGameStatus,
 | 
					    game: CurrentGameStatus,
 | 
				
			||||||
    curr_shoot_position: Coordinates,
 | 
					    curr_shoot_position: Coordinates,
 | 
				
			||||||
    last_opponent_fire_position: Coordinates,
 | 
					    last_opponent_fire_position: Coordinates,
 | 
				
			||||||
 | 
					    curr_button: usize,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl GameScreen {
 | 
					impl GameScreen {
 | 
				
			||||||
@@ -72,6 +107,7 @@ impl GameScreen {
 | 
				
			|||||||
            game: Default::default(),
 | 
					            game: Default::default(),
 | 
				
			||||||
            curr_shoot_position: Coordinates::new(0, 0),
 | 
					            curr_shoot_position: Coordinates::new(0, 0),
 | 
				
			||||||
            last_opponent_fire_position: Coordinates::invalid(),
 | 
					            last_opponent_fire_position: Coordinates::invalid(),
 | 
				
			||||||
 | 
					            curr_button: 0,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,6 +117,10 @@ impl GameScreen {
 | 
				
			|||||||
        let mut coordinates_mapper = CoordinatesMapper::new();
 | 
					        let mut coordinates_mapper = CoordinatesMapper::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        loop {
 | 
					        loop {
 | 
				
			||||||
 | 
					            if !self.visible_buttons().is_empty() {
 | 
				
			||||||
 | 
					                self.curr_button %= self.visible_buttons().len();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Update UI
 | 
					            // Update UI
 | 
				
			||||||
            terminal.draw(|f| coordinates_mapper = self.ui(f))?;
 | 
					            terminal.draw(|f| coordinates_mapper = self.ui(f))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -121,6 +161,36 @@ impl GameScreen {
 | 
				
			|||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // Change buttons
 | 
				
			||||||
 | 
					                        KeyCode::Left if self.game_over() => {
 | 
				
			||||||
 | 
					                            self.curr_button += self.visible_buttons().len() - 1
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        KeyCode::Right if self.game_over() => self.curr_button += 1,
 | 
				
			||||||
 | 
					                        KeyCode::Tab if self.game_over() => self.curr_button += 1,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // Submit button
 | 
				
			||||||
 | 
					                        KeyCode::Enter if self.game_over() => match self.curr_button() {
 | 
				
			||||||
 | 
					                            Buttons::RequestRematch => {
 | 
				
			||||||
 | 
					                                self.client
 | 
				
			||||||
 | 
					                                    .send_message(&ClientMessage::RequestRematch)
 | 
				
			||||||
 | 
					                                    .await?;
 | 
				
			||||||
 | 
					                                self.status = GameStatus::RematchRequestedByPlayer;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            Buttons::AcceptRematch => {
 | 
				
			||||||
 | 
					                                self.client
 | 
				
			||||||
 | 
					                                    .send_message(&ClientMessage::AcceptRematch)
 | 
				
			||||||
 | 
					                                    .await?;
 | 
				
			||||||
 | 
					                                self.status = GameStatus::RematchAccepted;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            Buttons::RejectRematch => {
 | 
				
			||||||
 | 
					                                self.client
 | 
				
			||||||
 | 
					                                    .send_message(&ClientMessage::RejectRematch)
 | 
				
			||||||
 | 
					                                    .await?;
 | 
				
			||||||
 | 
					                                self.status = GameStatus::RematchRejected;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            Buttons::QuitGame => return Ok(ScreenResult::Ok(())),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        _ => {}
 | 
					                        _ => {}
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -194,19 +264,41 @@ impl GameScreen {
 | 
				
			|||||||
                        self.game = status;
 | 
					                        self.game = status;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    ServerMessage::FireResult { .. } => {}
 | 
					                    ServerMessage::FireResult { .. } => { /* not used */ }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    ServerMessage::OpponentFireResult { pos, .. } => {
 | 
					                    ServerMessage::OpponentFireResult { pos, .. } => {
 | 
				
			||||||
                        self.last_opponent_fire_position = pos;
 | 
					                        self.last_opponent_fire_position = pos;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    ServerMessage::LostGame { .. } => {}
 | 
					                    ServerMessage::LostGame { status } => {
 | 
				
			||||||
                    ServerMessage::WonGame { .. } => {}
 | 
					                        self.game = status;
 | 
				
			||||||
                    ServerMessage::OpponentRequestedRematch => {}
 | 
					                        self.status = GameStatus::LostGame;
 | 
				
			||||||
                    ServerMessage::OpponentAcceptedRematch => {}
 | 
					                    }
 | 
				
			||||||
                    ServerMessage::OpponentRejectedRematch => {}
 | 
					
 | 
				
			||||||
                    ServerMessage::OpponentLeftGame => {}
 | 
					                    ServerMessage::WonGame { status } => {
 | 
				
			||||||
                    ServerMessage::OpponentReplacedByBot => {}
 | 
					                        self.game = status;
 | 
				
			||||||
 | 
					                        self.status = GameStatus::WonGame;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    ServerMessage::OpponentRequestedRematch => {
 | 
				
			||||||
 | 
					                        self.status = GameStatus::RematchRequestedByOpponent;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    ServerMessage::OpponentAcceptedRematch => {
 | 
				
			||||||
 | 
					                        self.status = GameStatus::RematchAccepted;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    ServerMessage::OpponentRejectedRematch => {
 | 
				
			||||||
 | 
					                        self.status = GameStatus::RematchRejected;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    ServerMessage::OpponentLeftGame => {
 | 
				
			||||||
 | 
					                        self.status = GameStatus::OpponentLeftGame;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    ServerMessage::OpponentReplacedByBot => {
 | 
				
			||||||
 | 
					                        PopupScreen::new("Opponent was replaced by a bot.").show(terminal)?;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -220,10 +312,37 @@ impl GameScreen {
 | 
				
			|||||||
        matches!(self.status, GameStatus::MustFire)
 | 
					        matches!(self.status, GameStatus::MustFire)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn game_over(&self) -> bool {
 | 
				
			||||||
 | 
					        self.game.is_game_over()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn visible_buttons(&self) -> Vec<Buttons> {
 | 
				
			||||||
 | 
					        let mut buttons = vec![];
 | 
				
			||||||
 | 
					        if self.game_over() && self.status != GameStatus::RematchAccepted {
 | 
				
			||||||
 | 
					            // Respond to rematch request / quit
 | 
				
			||||||
 | 
					            if self.status == GameStatus::RematchRequestedByOpponent {
 | 
				
			||||||
 | 
					                buttons.push(Buttons::AcceptRematch);
 | 
				
			||||||
 | 
					                buttons.push(Buttons::RejectRematch);
 | 
				
			||||||
 | 
					            } else if self.status != GameStatus::OpponentLeftGame
 | 
				
			||||||
 | 
					                && self.status != GameStatus::RematchRejected
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                buttons.push(Buttons::RequestRematch);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            buttons.push(Buttons::QuitGame);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        buttons
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn opponent_name(&self) -> &str {
 | 
					    fn opponent_name(&self) -> &str {
 | 
				
			||||||
        self.opponent_name.as_deref().unwrap_or("opponent")
 | 
					        self.opponent_name.as_deref().unwrap_or("opponent")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn curr_button(&self) -> Buttons {
 | 
				
			||||||
 | 
					        self.visible_buttons()[self.curr_button]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn player_map(&self, map: &CurrentGameMapStatus, opponent_map: bool) -> GameMapWidget {
 | 
					    fn player_map(&self, map: &CurrentGameMapStatus, opponent_map: bool) -> GameMapWidget {
 | 
				
			||||||
        let mut map_widget = GameMapWidget::new(&self.game.rules).set_default_empty_char(' ');
 | 
					        let mut map_widget = GameMapWidget::new(&self.game.rules).set_default_empty_char(' ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -341,6 +460,13 @@ impl GameScreen {
 | 
				
			|||||||
                .set_legend("Use arrows + Enter\nor click on the place\nwhere you want\nto shoot");
 | 
					                .set_legend("Use arrows + Enter\nor click on the place\nwhere you want\nto shoot");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Prepare buttons
 | 
				
			||||||
 | 
					        let buttons = self
 | 
				
			||||||
 | 
					            .visible_buttons()
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .map(|b| ButtonWidget::new(b.text(), self.curr_button() == *b))
 | 
				
			||||||
 | 
					            .collect::<Vec<_>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Show both maps if there is enough room on the screen
 | 
					        // Show both maps if there is enough room on the screen
 | 
				
			||||||
        let player_map_size = player_map.estimated_size();
 | 
					        let player_map_size = player_map.estimated_size();
 | 
				
			||||||
        let opponent_map_size = opponent_map.estimated_size();
 | 
					        let opponent_map_size = opponent_map.estimated_size();
 | 
				
			||||||
@@ -353,7 +479,9 @@ impl GameScreen {
 | 
				
			|||||||
            false => max(player_map_size.0, opponent_map_size.0),
 | 
					            false => max(player_map_size.0, opponent_map_size.0),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let max_width = max(maps_width, status_text.len() as u16);
 | 
					        let buttons_width = buttons.iter().fold(0, |a, b| a + b.estimated_size().0 + 4);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let max_width = max(maps_width, status_text.len() as u16).max(buttons_width);
 | 
				
			||||||
        let total_height = 3 + maps_height + 3;
 | 
					        let total_height = 3 + maps_height + 3;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Check if frame is too small
 | 
					        // Check if frame is too small
 | 
				
			||||||
@@ -399,7 +527,25 @@ impl GameScreen {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Render buttons
 | 
					        // Render buttons
 | 
				
			||||||
        // TODO : at the end of the game
 | 
					        if !buttons.is_empty() {
 | 
				
			||||||
 | 
					            let buttons_area = Layout::default()
 | 
				
			||||||
 | 
					                .direction(Direction::Horizontal)
 | 
				
			||||||
 | 
					                .constraints(
 | 
				
			||||||
 | 
					                    (0..buttons.len())
 | 
				
			||||||
 | 
					                        .map(|_| Constraint::Percentage(100 / buttons.len() as u16))
 | 
				
			||||||
 | 
					                        .collect::<Vec<_>>(),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .split(chunks[2]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (idx, b) in buttons.into_iter().enumerate() {
 | 
				
			||||||
 | 
					                let target = centered_rect_size(
 | 
				
			||||||
 | 
					                    b.estimated_size().0,
 | 
				
			||||||
 | 
					                    b.estimated_size().1,
 | 
				
			||||||
 | 
					                    &buttons_area[idx],
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                f.render_widget(b, target);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        coordinates_mapper
 | 
					        coordinates_mapper
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,14 +34,18 @@ impl ButtonWidget {
 | 
				
			|||||||
        self.min_width = min_width;
 | 
					        self.min_width = min_width;
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn estimated_size(&self) -> (u16, u16) {
 | 
				
			||||||
 | 
					        ((self.label.len() + 2).max(self.min_width) as u16, 1)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Widget for ButtonWidget {
 | 
					impl Widget for ButtonWidget {
 | 
				
			||||||
    fn render(self, area: Rect, buf: &mut Buffer) {
 | 
					    fn render(self, area: Rect, buf: &mut Buffer) {
 | 
				
			||||||
        let expected_len = (self.label.len() + 2).max(self.min_width);
 | 
					        let expected_len = self.estimated_size().0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut label = self.label.clone();
 | 
					        let mut label = self.label.clone();
 | 
				
			||||||
        while label.len() < expected_len {
 | 
					        while label.len() < expected_len as usize {
 | 
				
			||||||
            label.insert(0, ' ');
 | 
					            label.insert(0, ' ');
 | 
				
			||||||
            label.push(' ');
 | 
					            label.push(' ');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -300,6 +300,12 @@ impl CurrentGameStatus {
 | 
				
			|||||||
            BotType::Smart => self.find_smart_bot_fire_location(),
 | 
					            BotType::Smart => self.find_smart_bot_fire_location(),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Check out whether game is over or not
 | 
				
			||||||
 | 
					    pub fn is_game_over(&self) -> bool {
 | 
				
			||||||
 | 
					        self.opponent_map.sunk_boats.len() == self.rules.boats_list().len()
 | 
				
			||||||
 | 
					            || self.your_map.sunk_boats.len() == self.rules.boats_list().len()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[cfg(test)]
 | 
					#[cfg(test)]
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user