This repository has been archived on 2025-03-28. You can view files and clone it, but cannot push or open issues or pull requests.
SeaBattle/rust/cli_player/src/ui_screens/configure_game_rules.rs
Pierre Hubert 0280daf6d2
All checks were successful
continuous-integration/drone/push Build is passing
Fix appearance issues
2022-10-17 19:00:13 +02:00

301 lines
11 KiB
Rust

extern crate num as num_renamed;
use std::io;
use std::time::{Duration, Instant};
use crossterm::event;
use crossterm::event::{Event, KeyCode};
use sea_battle_backend::consts::{
MAX_BOATS_NUMBER, MAX_MAP_HEIGHT, MAX_MAP_WIDTH, MAX_STRIKE_TIMEOUT,
};
use tui::backend::Backend;
use tui::layout::{Constraint, Direction, Layout, Margin};
use tui::style::{Color, Style};
use tui::widgets::*;
use tui::{Frame, Terminal};
use sea_battle_backend::data::GameRules;
use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE};
use crate::ui_screens::popup_screen::show_screen_too_small_popup;
use crate::ui_screens::select_bot_type_screen::SelectBotTypeScreen;
use crate::ui_screens::utils::centered_rect_size;
use crate::ui_screens::ScreenResult;
use crate::ui_widgets::button_widget::ButtonWidget;
use crate::ui_widgets::checkbox_widget::CheckboxWidget;
use crate::ui_widgets::text_editor_widget::TextEditorWidget;
#[derive(num_derive::FromPrimitive, num_derive::ToPrimitive, Eq, PartialEq)]
enum EditingField {
MapWidth = 0,
MapHeight,
BoatsList,
StrikeTimeout,
BoatsCanTouch,
PlayerContinueOnHit,
BotType,
Cancel,
OK,
}
pub struct GameRulesConfigurationScreen {
rules: GameRules,
curr_field: EditingField,
}
impl GameRulesConfigurationScreen {
pub fn new(rules: GameRules) -> Self {
Self {
rules,
curr_field: EditingField::OK,
}
}
pub fn show<B: Backend>(
mut self,
terminal: &mut Terminal<B>,
) -> io::Result<ScreenResult<GameRules>> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| self.ui(f))?;
let timeout = TICK_RATE
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout)? {
let mut cursor_pos = self.curr_field as i32;
if let Event::Key(key) = event::read()? {
match key.code {
// Quit app
KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
// Navigate between fields
KeyCode::Up | KeyCode::Left => cursor_pos -= 1,
KeyCode::Down | KeyCode::Right | KeyCode::Tab => cursor_pos += 1,
// Submit results
KeyCode::Enter => {
if self.curr_field == EditingField::BotType {
if let ScreenResult::Ok(t) =
SelectBotTypeScreen::default().show(terminal)?
{
self.rules.bot_type = t;
}
}
if self.curr_field == EditingField::Cancel {
return Ok(ScreenResult::Canceled);
}
if self.curr_field == EditingField::OK && self.rules.is_valid() {
return Ok(ScreenResult::Ok(self.rules));
}
}
KeyCode::Char(' ') => {
if self.curr_field == EditingField::BoatsCanTouch {
self.rules.boats_can_touch = !self.rules.boats_can_touch;
}
if self.curr_field == EditingField::PlayerContinueOnHit {
self.rules.player_continue_on_hit =
!self.rules.player_continue_on_hit;
}
}
KeyCode::Backspace => {
if self.curr_field == EditingField::MapWidth {
self.rules.map_width /= 10;
}
if self.curr_field == EditingField::MapHeight {
self.rules.map_height /= 10;
}
if self.curr_field == EditingField::BoatsList
&& !self.rules.boats_list().is_empty()
{
self.rules.remove_last_boat();
}
if self.curr_field == EditingField::StrikeTimeout {
match self.rules.strike_timeout.unwrap_or(0) / 10 {
0 => self.rules.strike_timeout = None,
v => self.rules.strike_timeout = Some(v),
}
}
}
KeyCode::Char(c) if ('0'..='9').contains(&c) => {
let val = c.to_string().parse::<usize>().unwrap_or_default();
if self.curr_field == EditingField::MapWidth
&& self.rules.map_width <= MAX_MAP_WIDTH
{
self.rules.map_width *= 10;
self.rules.map_width += val;
}
if self.curr_field == EditingField::MapHeight
&& self.rules.map_height <= MAX_MAP_HEIGHT
{
self.rules.map_height *= 10;
self.rules.map_height += val;
}
if self.curr_field == EditingField::BoatsList
&& self.rules.boats_list().len() < MAX_BOATS_NUMBER
{
self.rules.add_boat(val);
}
if self.curr_field == EditingField::StrikeTimeout {
let mut timeout = self.rules.strike_timeout.unwrap_or(0);
if timeout <= MAX_STRIKE_TIMEOUT {
timeout *= 10;
timeout += val as u64;
self.rules.strike_timeout = Some(timeout);
}
}
}
_ => {}
}
}
// Apply new cursor position
self.curr_field = if cursor_pos < 0 {
EditingField::OK
} else {
num_renamed::FromPrimitive::from_u64(cursor_pos as u64)
.unwrap_or(EditingField::MapWidth)
}
}
if last_tick.elapsed() >= TICK_RATE {
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
let (w, h) = (50, 23);
if f.size().width < w || f.size().height < h {
show_screen_too_small_popup(f);
return;
}
let area = centered_rect_size(w, h, &f.size());
let block = Block::default()
.title("📓 Game rules")
.borders(Borders::ALL);
f.render_widget(block, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Map width
Constraint::Length(3), // Map height
Constraint::Length(3), // Boats list
Constraint::Length(3), // Strike timeout
Constraint::Length(1), // Boats can touch
Constraint::Length(1), // Player continue on hit
Constraint::Length(3), // Bot type
Constraint::Length(1), // Margin
Constraint::Length(1), // Buttons
Constraint::Length(1), // Error message (if any)
])
.split(area.inner(&Margin {
horizontal: 2,
vertical: 1,
}));
let editor = TextEditorWidget::new(
"Map width",
&self.rules.map_width.to_string(),
self.curr_field == EditingField::MapWidth,
);
f.render_widget(editor, chunks[EditingField::MapWidth as usize]);
let editor = TextEditorWidget::new(
"Map height",
&self.rules.map_height.to_string(),
self.curr_field == EditingField::MapHeight,
);
f.render_widget(editor, chunks[EditingField::MapHeight as usize]);
let editor = TextEditorWidget::new(
"Boats list",
&self
.rules
.boats_list()
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join("; "),
self.curr_field == EditingField::BoatsList,
);
f.render_widget(editor, chunks[EditingField::BoatsList as usize]);
let editor = TextEditorWidget::new(
"Strike timeout (0 to disable)",
&self.rules.strike_timeout.unwrap_or(0).to_string(),
self.curr_field == EditingField::StrikeTimeout,
);
f.render_widget(editor, chunks[EditingField::StrikeTimeout as usize]);
let editor = CheckboxWidget::new(
"Boats can touch",
self.rules.boats_can_touch,
self.curr_field == EditingField::BoatsCanTouch,
);
f.render_widget(editor, chunks[EditingField::BoatsCanTouch as usize]);
let editor = CheckboxWidget::new(
"Player continue on hit",
self.rules.player_continue_on_hit,
self.curr_field == EditingField::PlayerContinueOnHit,
);
f.render_widget(editor, chunks[EditingField::PlayerContinueOnHit as usize]);
// Select bot type
let bot_type_text = format!("Bot type: {}", self.rules.bot_type.description().name);
let text = Paragraph::new(bot_type_text.as_str()).style(
match self.curr_field == EditingField::BotType {
false => Style::default(),
true => Style::default().fg(HIGHLIGHT_COLOR),
},
);
f.render_widget(
text,
chunks[EditingField::BotType as usize].inner(&Margin {
horizontal: 0,
vertical: 1,
}),
);
// Buttons
let buttons_chunk = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[EditingField::OK as usize]);
let button = ButtonWidget::new("Cancel", self.curr_field == EditingField::Cancel);
f.render_widget(button, buttons_chunk[0]);
let button = ButtonWidget::new("OK", self.curr_field == EditingField::OK)
.set_disabled(!self.rules.is_valid());
f.render_widget(button, buttons_chunk[1]);
// Error message (if any)
if let Some(msg) = self.rules.get_errors().first() {
let area = centered_rect_size(msg.len() as u16, 1, chunks.last().unwrap());
let err = Paragraph::new(*msg).style(Style::default().fg(Color::Red));
f.render_widget(err, area);
}
}
}