-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
381 additions
and
66 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
use std::{ | ||
sync::{ | ||
mpsc::{channel, Receiver, Sender}, | ||
Arc, Mutex, | ||
}, | ||
thread::{self, JoinHandle}, | ||
}; | ||
|
||
use anyhow::Error; | ||
|
||
use ratatui::{ | ||
crossterm::event::{self, Event, KeyCode}, | ||
layout::{Alignment, Constraint, Flex, Layout, Position}, | ||
style::{Style, Stylize}, | ||
text::{Line, Text}, | ||
widgets::{block, Cell, Clear, Padding, Paragraph, Row, Table, TableState}, | ||
DefaultTerminal, Frame, | ||
}; | ||
|
||
mod widgets; | ||
|
||
use widgets::UserInput; | ||
|
||
use crate::bot::{Bot, QLearning}; | ||
|
||
const EXIT: &str = "\ | ||
exiting will remove all bots and erase their memory. | ||
are you sure you want to exit? | ||
leave go back | ||
(Enter) (Esc) | ||
"; | ||
|
||
enum PopupMenu { | ||
BotCreation, | ||
Error(String), | ||
Exit, | ||
} | ||
|
||
type Worker = (String, Sender<()>, JoinHandle<Result<(), Error>>); | ||
|
||
fn worker( | ||
mut env: Bot, | ||
policy: Arc<Mutex<QLearning>>, | ||
interrupt: Receiver<()>, | ||
) -> Result<(), Error> { | ||
loop { | ||
if interrupt.try_recv().is_ok() { | ||
return Ok(()); | ||
} | ||
let (mut state1, mut masks1) = env.reset()?; | ||
loop { | ||
let action = { | ||
let mut policy = policy.lock().expect("sample lock"); | ||
policy.sample(state1.clone(), masks1.clone()) | ||
}; | ||
let (state2, masks2, reward, done) = env.step(action.clone())?; | ||
if done { | ||
let mut policy = policy.lock().expect("done lock"); | ||
policy.update_done(state1.clone(), action.clone(), reward); | ||
break; | ||
} | ||
{ | ||
let mut policy = policy.lock().expect("step lock"); | ||
policy.update_step( | ||
state1.clone(), | ||
action.clone(), | ||
reward, | ||
state2.clone(), | ||
masks2.clone(), | ||
); | ||
} | ||
state1.clone_from(&state2); | ||
masks1.clone_from(&masks2); | ||
} | ||
} | ||
} | ||
|
||
pub struct App { | ||
addr: String, | ||
policy: Arc<Mutex<QLearning>>, | ||
workers: Vec<Worker>, | ||
table_state: TableState, | ||
user_input: UserInput, | ||
popup_menu: Option<PopupMenu>, | ||
} | ||
|
||
impl App { | ||
pub fn new(addr: String, policy: Arc<Mutex<QLearning>>) -> Self { | ||
Self { | ||
addr, | ||
policy, | ||
workers: Vec::new(), | ||
table_state: TableState::new(), | ||
user_input: UserInput::new(), | ||
popup_menu: None, | ||
} | ||
} | ||
|
||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<(), Error> { | ||
loop { | ||
terminal.draw(|frame| self.draw(frame))?; | ||
|
||
if let Event::Key(key) = event::read()? { | ||
match self.popup_menu { | ||
Some(PopupMenu::BotCreation) => match key.code { | ||
KeyCode::Char(to_insert) => self.user_input.input(to_insert), | ||
KeyCode::Delete => self.user_input.delete(), | ||
KeyCode::Backspace => self.user_input.backspace(), | ||
KeyCode::Left => self.user_input.move_left(), | ||
KeyCode::Right => self.user_input.move_right(), | ||
KeyCode::Home => self.user_input.jump_to_first(), | ||
KeyCode::End => self.user_input.jump_to_last(), | ||
KeyCode::Enter if !self.user_input.value.is_empty() => { | ||
let botname = self.user_input.submit(); | ||
let addr = self.addr.clone(); | ||
match Bot::new(&botname, &addr) { | ||
Ok(env) => { | ||
let policy = self.policy.clone(); | ||
let (tx_server, rx_worker): (Sender<()>, Receiver<()>) = | ||
channel(); | ||
self.workers.push(( | ||
botname.clone(), | ||
tx_server, | ||
thread::spawn(move || worker(env, policy, rx_worker)), | ||
)); | ||
self.table_state.select(Some(self.workers.len() - 1)); | ||
self.popup_menu = None; | ||
} | ||
Err(msg) => { | ||
self.popup_menu = Some(PopupMenu::Error(msg.to_string())) | ||
} | ||
} | ||
} | ||
KeyCode::Esc => { | ||
self.user_input.clear(); | ||
self.popup_menu = None; | ||
} | ||
_ => {} | ||
}, | ||
Some(PopupMenu::Error(_)) => self.popup_menu = None, | ||
Some(PopupMenu::Exit) => match key.code { | ||
KeyCode::Enter => return Ok(()), | ||
KeyCode::Esc => self.popup_menu = None, | ||
_ => {} | ||
}, | ||
None => match key.code { | ||
KeyCode::Char('d') if self.table_state.selected().is_some() => { | ||
if let Some(idx) = self.table_state.selected() { | ||
let (_, tx_server, _) = self.workers.remove(idx); | ||
tx_server.send(())?; | ||
} | ||
} | ||
KeyCode::Char('i') => self.popup_menu = Some(PopupMenu::BotCreation), | ||
KeyCode::Esc => self.popup_menu = Some(PopupMenu::Exit), | ||
KeyCode::Down => self.table_state.select_next(), | ||
KeyCode::Up => self.table_state.select_previous(), | ||
_ => {} | ||
}, | ||
} | ||
} | ||
|
||
// Only keep workers that're doing work. | ||
self.workers.retain(|(.., handle)| !handle.is_finished()); | ||
} | ||
} | ||
|
||
fn draw(&mut self, frame: &mut Frame) { | ||
let window = Layout::vertical([Constraint::Length(1), Constraint::Min(6)]); | ||
let [help_area, table_area] = window.areas(frame.area()); | ||
|
||
// Render current bots. | ||
let table = Table::new( | ||
self.workers.iter().map(|(botname, ..)| { | ||
let text = Text::raw(botname); | ||
let cell = Cell::new(text); | ||
Row::new([cell]) | ||
}), | ||
[Constraint::Fill(1)], | ||
) | ||
.block(block::Block::bordered().padding(Padding::uniform(1))) | ||
.highlight_style(Style::new().bg(ratatui::style::Color::White)); | ||
frame.render_stateful_widget(table, table_area, &mut self.table_state); | ||
|
||
// Render user input help message. | ||
let help_message = vec![ | ||
"press ".into(), | ||
"i".bold().white(), | ||
" to create a bot, ".into(), | ||
"up/down".bold().white(), | ||
" to select a bot, ".into(), | ||
"d".bold().white(), | ||
" delete a bot, or press ".into(), | ||
"Esc".bold().white(), | ||
" to exit".into(), | ||
]; | ||
let help_style = Style::default(); | ||
let help_message = Text::from(Line::from(help_message)).patch_style(help_style); | ||
let help_message = Paragraph::new(help_message); | ||
frame.render_widget(help_message, help_area); | ||
|
||
// Render popup menus. | ||
match self.popup_menu { | ||
Some(PopupMenu::BotCreation) => { | ||
let vertical = Layout::vertical([Constraint::Length(3)]).flex(Flex::Center); | ||
let horizontal = Layout::horizontal([Constraint::Max(60)]).flex(Flex::Center); | ||
let [user_input_area] = vertical.areas(frame.area()); | ||
let [user_input_area] = horizontal.areas(user_input_area); | ||
frame.render_widget(Clear, user_input_area); // clears out the background | ||
|
||
let user_input = Paragraph::new(self.user_input.value.as_str()) | ||
.style(Style::default()) | ||
.block(block::Block::bordered().title(" create a new bot ")); | ||
frame.render_widget(user_input, user_input_area); | ||
frame.set_cursor_position(Position::new( | ||
// Draw the cursor at the current position in the input field. | ||
// This position is can be controlled via the left and right arrow key | ||
user_input_area.x + self.user_input.char_idx as u16 + 1, | ||
// Move one line down, from the border to the input line | ||
user_input_area.y + 1, | ||
)); | ||
} | ||
Some(PopupMenu::Error(ref msg)) => { | ||
let vertical = Layout::vertical([Constraint::Max(8)]).flex(Flex::Center); | ||
let horizontal = Layout::horizontal([Constraint::Max(60)]).flex(Flex::Center); | ||
let [error_menu_area] = vertical.areas(frame.area()); | ||
let [error_menu_area] = horizontal.areas(error_menu_area); | ||
frame.render_widget(Clear, error_menu_area); // clears out the background | ||
|
||
// Render error text. | ||
let error_text = | ||
Paragraph::new(format!("{}\n\n\npress any key to continue", msg.clone())) | ||
.style(Style::default()) | ||
.block( | ||
block::Block::bordered() | ||
.padding(Padding::uniform(1)) | ||
.title(" error "), | ||
) | ||
.alignment(Alignment::Center); | ||
frame.render_widget(error_text, error_menu_area); | ||
} | ||
Some(PopupMenu::Exit) => { | ||
let vertical = Layout::vertical([Constraint::Max(10)]).flex(Flex::Center); | ||
let horizontal = Layout::horizontal([Constraint::Max(60)]).flex(Flex::Center); | ||
let [exit_menu_area] = vertical.areas(frame.area()); | ||
let [exit_menu_area] = horizontal.areas(exit_menu_area); | ||
frame.render_widget(Clear, exit_menu_area); // clears out the background | ||
|
||
// Render exit text. | ||
let exit_text = Paragraph::new(EXIT) | ||
.style(Style::default()) | ||
.block(block::Block::bordered().padding(Padding::uniform(1))) | ||
.alignment(Alignment::Center); | ||
frame.render_widget(exit_text, exit_menu_area); | ||
} | ||
None => {} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
use private_poker::constants::MAX_USER_INPUT_LENGTH; | ||
|
||
/// Manages user inputs at the terminal. | ||
pub struct UserInput { | ||
/// Position of cursor in the input box. | ||
pub char_idx: usize, | ||
/// Current value of the input box. | ||
pub value: String, | ||
} | ||
|
||
impl UserInput { | ||
pub fn backspace(&mut self) { | ||
// Method "remove" is not used on the saved text for deleting the selected char. | ||
// Reason: Using remove on String works on bytes instead of the chars. | ||
// Using remove would require special care because of char boundaries. | ||
if self.char_idx != 0 { | ||
// Getting all characters before the selected character. | ||
let before_char_to_delete = self.value.chars().take(self.char_idx - 1); | ||
// Getting all characters after selected character. | ||
let after_char_to_delete = self.value.chars().skip(self.char_idx); | ||
|
||
// Put all characters together except the selected one. | ||
// By leaving the selected one out, it is forgotten and therefore deleted. | ||
self.value = before_char_to_delete.chain(after_char_to_delete).collect(); | ||
self.move_left(); | ||
} | ||
} | ||
|
||
/// Returns the byte index based on the character position. | ||
/// | ||
/// Since each character in a string can be contain multiple bytes, it's necessary to calculate | ||
/// the byte index based on the index of the character. | ||
fn byte_idx(&self) -> usize { | ||
self.value | ||
.char_indices() | ||
.map(|(i, _)| i) | ||
.nth(self.char_idx) | ||
.unwrap_or(self.value.len()) | ||
} | ||
|
||
pub fn clear(&mut self) { | ||
self.char_idx = 0; | ||
self.value.clear(); | ||
} | ||
|
||
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize { | ||
new_cursor_pos.clamp(0, self.value.chars().count()) | ||
} | ||
|
||
pub fn delete(&mut self) { | ||
// Method "remove" is not used on the saved text for deleting the selected char. | ||
// Reason: Using remove on String works on bytes instead of the chars. | ||
// Using remove would require special care because of char boundaries. | ||
if self.char_idx != self.value.len() { | ||
// Getting all characters before the selected character. | ||
let before_char_to_delete = self.value.chars().take(self.char_idx); | ||
// Getting all characters after selected character. | ||
let after_char_to_delete = self.value.chars().skip(self.char_idx + 1); | ||
|
||
// Put all characters together except the selected one. | ||
// By leaving the selected one out, it is forgotten and therefore deleted. | ||
self.value = before_char_to_delete.chain(after_char_to_delete).collect(); | ||
} | ||
} | ||
|
||
pub fn input(&mut self, new_char: char) { | ||
// Username length is about the same size as the largest allowed | ||
if self.value.len() < MAX_USER_INPUT_LENGTH { | ||
let idx = self.byte_idx(); | ||
self.value.insert(idx, new_char); | ||
self.move_right(); | ||
} | ||
} | ||
|
||
pub fn jump_to_first(&mut self) { | ||
self.char_idx = 0; | ||
} | ||
|
||
pub fn jump_to_last(&mut self) { | ||
self.char_idx = self.value.len(); | ||
} | ||
|
||
pub fn move_left(&mut self) { | ||
let cursor_moved_left = self.char_idx.saturating_sub(1); | ||
self.char_idx = self.clamp_cursor(cursor_moved_left); | ||
} | ||
|
||
pub fn move_right(&mut self) { | ||
let cursor_moved_right = self.char_idx.saturating_add(1); | ||
self.char_idx = self.clamp_cursor(cursor_moved_right); | ||
} | ||
|
||
pub fn new() -> Self { | ||
Self { | ||
char_idx: 0, | ||
value: String::new(), | ||
} | ||
} | ||
|
||
pub fn submit(&mut self) -> String { | ||
let input = self.value.clone(); | ||
self.char_idx = 0; | ||
self.value.clear(); | ||
input | ||
} | ||
} |
Oops, something went wrong.