Compare commits
4 Commits
3455559d33
...
f2ec85b46f
Author | SHA1 | Date | |
---|---|---|---|
f2ec85b46f | |||
a2c880814c | |||
b832ef82ed | |||
4341bdc682 |
@@ -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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,6 +94,8 @@ pub struct GameScreen {
|
|||||||
opponent_name: Option<String>,
|
opponent_name: Option<String>,
|
||||||
game: CurrentGameStatus,
|
game: CurrentGameStatus,
|
||||||
curr_shoot_position: Coordinates,
|
curr_shoot_position: Coordinates,
|
||||||
|
last_opponent_fire_position: Coordinates,
|
||||||
|
curr_button: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameScreen {
|
impl GameScreen {
|
||||||
@@ -70,6 +106,8 @@ impl GameScreen {
|
|||||||
opponent_name: None,
|
opponent_name: None,
|
||||||
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(),
|
||||||
|
curr_button: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,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))?;
|
||||||
|
|
||||||
@@ -119,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(())),
|
||||||
|
},
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,16 +264,41 @@ impl GameScreen {
|
|||||||
self.game = status;
|
self.game = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::FireResult { .. } => {}
|
ServerMessage::FireResult { .. } => { /* not used */ }
|
||||||
ServerMessage::OpponentFireResult { .. } => {}
|
|
||||||
|
|
||||||
ServerMessage::LostGame { .. } => {}
|
ServerMessage::OpponentFireResult { pos, .. } => {
|
||||||
ServerMessage::WonGame { .. } => {}
|
self.last_opponent_fire_position = pos;
|
||||||
ServerMessage::OpponentRequestedRematch => {}
|
}
|
||||||
ServerMessage::OpponentAcceptedRematch => {}
|
|
||||||
ServerMessage::OpponentRejectedRematch => {}
|
ServerMessage::LostGame { status } => {
|
||||||
ServerMessage::OpponentLeftGame => {}
|
self.game = status;
|
||||||
ServerMessage::OpponentReplacedByBot => {}
|
self.status = GameStatus::LostGame;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerMessage::WonGame { status } => {
|
||||||
|
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)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,27 +312,72 @@ 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);
|
let mut map_widget = GameMapWidget::new(&self.game.rules).set_default_empty_char(' ');
|
||||||
|
|
||||||
// Current shoot position
|
// Current shoot position
|
||||||
if opponent_map {
|
if opponent_map {
|
||||||
map_widget = map_widget.add_colored_cells(ColoredCells {
|
map_widget = map_widget.add_colored_cells(ColoredCells {
|
||||||
color: match self.game.can_fire_at_location(self.curr_shoot_position) {
|
color: match (
|
||||||
true => Color::Green,
|
self.game.can_fire_at_location(self.curr_shoot_position),
|
||||||
false => Color::LightYellow,
|
self.game
|
||||||
|
.opponent_map
|
||||||
|
.successful_strikes
|
||||||
|
.contains(&self.curr_shoot_position),
|
||||||
|
) {
|
||||||
|
(true, _) => Color::Green,
|
||||||
|
(false, false) => Color::LightYellow,
|
||||||
|
(false, true) => Color::LightRed,
|
||||||
},
|
},
|
||||||
cells: vec![self.curr_shoot_position],
|
cells: vec![self.curr_shoot_position],
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
map_widget = map_widget.add_colored_cells(ColoredCells {
|
||||||
|
color: Color::Green,
|
||||||
|
cells: vec![self.last_opponent_fire_position],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sunk boats
|
// Sunk boats
|
||||||
|
for b in &map.sunk_boats {
|
||||||
|
for c in b.all_coordinates() {
|
||||||
|
map_widget =
|
||||||
|
map_widget.set_char(c, b.len.to_string().chars().next().unwrap_or('9'));
|
||||||
|
}
|
||||||
|
}
|
||||||
let sunk_boats = ColoredCells {
|
let sunk_boats = ColoredCells {
|
||||||
color: Color::Gray,
|
color: Color::LightRed,
|
||||||
cells: map
|
cells: map
|
||||||
.sunk_boats
|
.sunk_boats
|
||||||
.iter()
|
.iter()
|
||||||
@@ -244,18 +386,29 @@ impl GameScreen {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Touched boats
|
// Touched boats
|
||||||
|
for b in &map.successful_strikes {
|
||||||
|
map_widget = map_widget.set_char_no_overwrite(*b, 'T');
|
||||||
|
}
|
||||||
let touched_areas = ColoredCells {
|
let touched_areas = ColoredCells {
|
||||||
color: Color::Red,
|
color: Color::Red,
|
||||||
cells: map.successful_strikes.clone(),
|
cells: map.successful_strikes.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Failed strikes
|
// Failed strikes
|
||||||
|
for b in &map.failed_strikes {
|
||||||
|
map_widget = map_widget.set_char_no_overwrite(*b, '.');
|
||||||
|
}
|
||||||
let failed_strikes = ColoredCells {
|
let failed_strikes = ColoredCells {
|
||||||
color: Color::DarkGray,
|
color: Color::Black,
|
||||||
cells: map.failed_strikes.clone(),
|
cells: map.failed_strikes.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Boats
|
// Boats
|
||||||
|
for b in &map.boats.0 {
|
||||||
|
for c in b.all_coordinates() {
|
||||||
|
map_widget = map_widget.set_char_no_overwrite(c, 'B');
|
||||||
|
}
|
||||||
|
}
|
||||||
let boats = ColoredCells {
|
let boats = ColoredCells {
|
||||||
color: Color::Blue,
|
color: Color::Blue,
|
||||||
cells: map
|
cells: map
|
||||||
@@ -307,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();
|
||||||
@@ -319,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
|
||||||
@@ -365,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(' ');
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use tui::buffer::Buffer;
|
use tui::buffer::Buffer;
|
||||||
@@ -21,6 +22,7 @@ pub struct GameMapWidget<'a> {
|
|||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
legend: Option<String>,
|
legend: Option<String>,
|
||||||
yield_coordinates: Option<Box<dyn 'a + FnMut(Coordinates, Rect)>>,
|
yield_coordinates: Option<Box<dyn 'a + FnMut(Coordinates, Rect)>>,
|
||||||
|
chars: HashMap<Coordinates, char>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> GameMapWidget<'a> {
|
impl<'a> GameMapWidget<'a> {
|
||||||
@@ -32,6 +34,7 @@ impl<'a> GameMapWidget<'a> {
|
|||||||
title: None,
|
title: None,
|
||||||
legend: None,
|
legend: None,
|
||||||
yield_coordinates: None,
|
yield_coordinates: None,
|
||||||
|
chars: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +66,16 @@ impl<'a> GameMapWidget<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_char(mut self, coordinates: Coordinates, c: char) -> Self {
|
||||||
|
self.chars.insert(coordinates, c);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_char_no_overwrite(mut self, coordinates: Coordinates, c: char) -> Self {
|
||||||
|
self.chars.entry(coordinates).or_insert(c);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn grid_size(&self) -> (u16, u16) {
|
pub fn grid_size(&self) -> (u16, u16) {
|
||||||
let w = self.rules.map_width as u16 * 2 + 1;
|
let w = self.rules.map_width as u16 * 2 + 1;
|
||||||
let h = self.rules.map_height as u16 * 2 + 1;
|
let h = self.rules.map_height as u16 * 2 + 1;
|
||||||
@@ -157,9 +170,12 @@ impl<'a> Widget for GameMapWidget<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if x < self.rules.map_width && y < self.rules.map_height {
|
if x < self.rules.map_width && y < self.rules.map_height {
|
||||||
let cell = buf
|
let cell = buf.get_mut(o_x + 1, o_y + 1).set_char(
|
||||||
.get_mut(o_x + 1, o_y + 1)
|
*self
|
||||||
.set_char(self.default_empty_character);
|
.chars
|
||||||
|
.get(&coordinates)
|
||||||
|
.unwrap_or(&self.default_empty_character),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(c) = color {
|
if let Some(c) = color {
|
||||||
cell.set_bg(c.color);
|
cell.set_bg(c.color);
|
||||||
|
@@ -85,6 +85,10 @@ impl Coordinates {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn invalid() -> Self {
|
||||||
|
Self { x: -1, y: -1 }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_valid(&self, rules: &GameRules) -> bool {
|
pub fn is_valid(&self, rules: &GameRules) -> bool {
|
||||||
self.x >= 0
|
self.x >= 0
|
||||||
&& self.y >= 0
|
&& self.y >= 0
|
||||||
|
@@ -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