Can request & respond to rematch

This commit is contained in:
Pierre HUBERT 2022-10-15 17:46:14 +02:00
parent a2c880814c
commit f2ec85b46f
3 changed files with 168 additions and 12 deletions

View File

@ -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
} }

View File

@ -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(' ');
} }

View File

@ -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)]