Add termion dependency to manage UI

This commit is contained in:
2020-11-15 21:14:18 +01:00
parent 7f2ec8e957
commit 11e3c7d48e
5 changed files with 363 additions and 173 deletions

View File

@ -1,14 +1,22 @@
extern crate termion;
use clichess::{ClientRequest, GameInfo, RecvPositionError, UserRole, EXIT_MSG};
use shakmaty::{Chess, Color, Outcome, Position, Setup};
use shakmaty::{fen, Chess, Color, Outcome, Position, Setup};
use std::io;
use std::os::unix::net::UnixStream;
use std::sync::mpsc::{channel, Receiver, TryRecvError};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;
use std::time;
use termion::event::Key;
use std::io::{stdin, stdout, Read, Write};
use termion::raw::{IntoRawMode, RawTerminal};
struct Client {
player: clichess::Player,
side: Color,
display_sender: Sender<DisplayMessage>,
waiting_server_msg_receiver: Receiver<WaitingServerMsg>,
keyboard_input_recv: Receiver<String>,
server_message_recv: Receiver<String>,
opponent_name: Option<String>,
@ -28,9 +36,15 @@ fn main() {
return;
}
};
let keyboard_input_recv = setup_keyboard_input_recv();
let (display_sender, display_receiver) = channel();
let (waiting_server_msg_sender, waiting_server_msg_receiver) = channel();
let keyboard_input_recv =
start_keyboard_input_thread(display_sender.clone(), waiting_server_msg_sender.clone());
start_display_thread(display_receiver);
//start a thread to listen to server messages
let server_message_recv = setup_server_message_recv(&stream).unwrap();
let server_message_recv =
setup_server_message_recv(&stream, waiting_server_msg_sender.clone()).unwrap();
let mut client = Client {
player: clichess::Player {
@ -39,6 +53,8 @@ fn main() {
public_key,
},
side: Color::White,
display_sender: display_sender.clone(),
waiting_server_msg_receiver,
keyboard_input_recv,
server_message_recv,
opponent_name: Option::None,
@ -54,34 +70,50 @@ fn main() {
client.side = Color::Black;
}
if client.player.role == UserRole::Spectator {
println!(
"Hello, {} !\n\r You're spectating !",
client.player.username
)
client
.display_sender
.send(DisplayMessage::Information(format!(
"Hello, {} !\n\r You're spectating !",
client.player.username
)))
.unwrap();
} else {
println!(
"Hello, {} !\n\r You're playing with the {} pieces",
client.player.username,
client.player.role.to_string()
);
client
.display_sender
.send(DisplayMessage::Information(format!(
"Hello, {} !\n\r You're playing with the {} pieces",
client.player.username,
client.player.role.to_string()
)))
.unwrap();
}
let mut current_position = fetch_initial_chess_position(&client, &mut stream);
loop {
let is_player_turn = clichess::is_player_turn(&client.player, current_position.turn());
if is_player_turn || client.player.role == UserRole::Spectator {
let mut message = String::from("");
client
.opponent_name
.clone()
.map(|name| print!("{} played", name));
.map(|name| message.push_str(&format!("{} played", name)));
client
.last_move
.clone()
.map(|chessmove| println!(" {}", chessmove));
.map(|chessmove| message.push_str(&format!(" {}", chessmove)));
if message.len() > 0 {
display_sender
.send(DisplayMessage::Information(message))
.unwrap();
}
}
println!(
"{}",
clichess::board_representation(&current_position, client.side)
);
display_sender
.send(DisplayMessage::Chessboard {
fen: fen::fen(&current_position),
side: client.side.clone(),
})
.unwrap();
//check if game is over.
if current_position.is_game_over() {
break;
@ -91,11 +123,14 @@ fn main() {
let mut input = client.keyboard_input_recv.recv().unwrap();
input = String::from(input.trim());
if input == EXIT_MSG {
client.waiting_server_msg_receiver.recv().unwrap();
break;
}
let response = send_request(&client, &mut stream, ClientRequest::Play(input.clone()));
if response == String::from("KO") {
println!("Invalid move: {}", input);
display_sender
.send(DisplayMessage::Message(format!("Invalid move: {}", input)))
.unwrap();
//go back to taking user input
continue;
}
@ -111,18 +146,38 @@ fn main() {
};
}
}
match current_position.outcome() {
None => println!("Bye"),
Some(Outcome::Draw) => println!("Draw game."),
client.display_sender.send(DisplayMessage::Clear).unwrap();
let end_message = match current_position.outcome() {
None => "Bye",
Some(Outcome::Draw) => "Draw game.",
Some(Outcome::Decisive { winner }) => {
if winner == Color::White {
println!("White has won the game.")
"White has won the game."
} else {
println!("Black has won the game.")
"Black has won the game."
}
}
}
.to_string();
client
.display_sender
.send(DisplayMessage::Message(end_message))
.unwrap();
thread::sleep(time::Duration::from_secs(1));
client
.display_sender
.send(DisplayMessage::Debug(
"sending request to server to exit...".to_string(),
))
.unwrap();
send_request(&client, &mut stream, ClientRequest::Exit);
client
.display_sender
.send(DisplayMessage::Debug(
"request to server to exit sent.".to_string(),
))
.unwrap();
client.display_sender.send(DisplayMessage::Exit).unwrap();
}
#[derive(PartialEq)]
@ -163,27 +218,157 @@ fn send_request(client: &Client, stream: &mut UnixStream, request: ClientRequest
}
Err(e) => {
println!("Error when parsing client request: {}", e);
client
.display_sender
.send(DisplayMessage::Message(format!(
"Error when parsing client request: {}",
e
)))
.unwrap();
response = String::from("KO");
}
};
response
}
fn setup_keyboard_input_recv() -> Receiver<String> {
#[derive(PartialEq, Clone)]
pub enum DisplayMessage {
Chessboard { fen: String, side: Color },
Message(String),
Information(String),
Debug(String),
Input(Key),
Enter,
Clear,
Exit,
}
fn start_display_thread(request_recv: Receiver<DisplayMessage>) {
thread::spawn(move || {
let stdout = stdout();
let mut stdout = stdout.lock().into_raw_mode().unwrap();
write!(
stdout,
"{}{}",
termion::clear::All,
termion::cursor::Goto(3, 17)
)
.unwrap();
stdout.flush().unwrap();
loop {
let msg = request_recv.recv().unwrap();
match msg {
DisplayMessage::Chessboard { fen, side } => {
let current_position = clichess::parse_position(&fen);
clichess::print_board_representation(&current_position, side, &mut stdout);
}
DisplayMessage::Message(s) => write!(
stdout,
"{}{}{}{}{}",
termion::cursor::Save,
termion::cursor::Goto(3, 20),
termion::clear::CurrentLine,
s,
termion::cursor::Restore
)
.unwrap(),
DisplayMessage::Information(s) => write!(
stdout,
"{}{}{}{}{}",
termion::cursor::Save,
termion::cursor::Goto(3, 3),
termion::clear::CurrentLine,
s,
termion::cursor::Restore
)
.unwrap(),
DisplayMessage::Clear => write!(stdout, "{}", termion::clear::All).unwrap(),
DisplayMessage::Enter => write!(
stdout,
"{}{}",
termion::clear::CurrentLine,
termion::cursor::Goto(3, 17)
)
.unwrap(),
DisplayMessage::Input(k) => display_key(&mut stdout, k),
DisplayMessage::Debug(s) => write!(
stdout,
"{}{}{}{}{}",
termion::cursor::Save,
termion::cursor::Goto(3, 25),
termion::clear::CurrentLine,
s,
termion::cursor::Restore
)
.unwrap(),
DisplayMessage::Exit => break,
}
stdout.flush().unwrap();
}
write!(stdout, "{}", termion::clear::All,).unwrap();
stdout.flush().unwrap();
});
}
fn start_keyboard_input_thread(
display_sender: Sender<DisplayMessage>,
waiting_server_msg_sender: Sender<WaitingServerMsg>,
) -> Receiver<String> {
let (sender, receiver) = channel();
thread::spawn(move || {
let mut buffer = String::new();
let stdin = stdin();
let stdin = stdin.lock();
let mut bytes = stdin.bytes();
loop {
let mut buffer = String::new();
//wait for user input
io::stdin().read_line(&mut buffer).unwrap();
sender.send(buffer).unwrap();
let b = bytes.next().unwrap().unwrap();
match b {
b'q' => {
sender.send(String::from(EXIT_MSG)).unwrap();
waiting_server_msg_sender
.send(WaitingServerMsg::UserCanceled)
.unwrap();
break;
}
b'\r' => {
display_sender.send(DisplayMessage::Enter).unwrap();
sender.send(buffer.clone()).unwrap();
if buffer == EXIT_MSG {
waiting_server_msg_sender
.send(WaitingServerMsg::UserCanceled)
.unwrap();
}
buffer.clear();
}
b => {
let c = b as char;
buffer.push_str(&c.to_string());
display_sender
.send(DisplayMessage::Input(Key::Char(c)))
.unwrap();
}
}
}
});
receiver
}
fn setup_server_message_recv(stream: &UnixStream) -> io::Result<Receiver<String>> {
fn display_key(stdout: &mut RawTerminal<io::StdoutLock>, key: Key) {
match key {
Key::Char(c) => write!(stdout, "{}", c).unwrap(),
_ => {}
};
}
enum WaitingServerMsg {
MsgReceived,
UserCanceled,
}
fn setup_server_message_recv(
stream: &UnixStream,
waiting_server_msg_sender: Sender<WaitingServerMsg>,
) -> io::Result<Receiver<String>> {
let (sender, receiver) = channel();
let thread_stream = stream.try_clone()?;
@ -193,6 +378,9 @@ fn setup_server_message_recv(stream: &UnixStream) -> io::Result<Receiver<String>
let buffer =
clichess::read_line_from_stream(&thread_stream).expect("Error message from server");
sender.send(buffer).unwrap();
waiting_server_msg_sender
.send(WaitingServerMsg::MsgReceived)
.unwrap();
}
});
Ok(receiver)
@ -217,24 +405,15 @@ fn wait_for_next_move(
client: &mut Client,
stream: &mut UnixStream,
) -> Result<Chess, RecvPositionError> {
println!("Waiting for opponent move...");
client
.display_sender
.send(DisplayMessage::Information(String::from(
"Waiting for opponent move...",
)))
.unwrap();
send_request(client, stream, ClientRequest::WaitForNextMove);
let response;
loop {
thread::sleep(time::Duration::from_millis(10));
{
let server_response_recv = &client.server_message_recv;
match server_response_recv.try_recv() {
Ok(msg) => {
response = msg;
break;
}
Err(TryRecvError::Disconnected) => println!("Error: server disconnected."),
Err(TryRecvError::Empty) => continue,
}
}
}
if response.is_empty() {
let response = fetch_message_from_server(client);
if response == EXIT_MSG {
Err(RecvPositionError::UserCanceledError)
} else {
let game_info: GameInfo = serde_json::from_str(&response).unwrap();
@ -253,22 +432,10 @@ fn update_client(game_info: &GameInfo, client: &mut Client) {
}
fn fetch_message_from_server(client: &Client) -> String {
let response;
loop {
thread::sleep(time::Duration::from_millis(10));
{
let server_response_recv = &client.server_message_recv;
match server_response_recv.try_recv() {
Ok(msg) => {
response = msg;
break;
}
Err(TryRecvError::Disconnected) => println!("Error: server disconnected."),
Err(TryRecvError::Empty) => continue,
}
}
match client.waiting_server_msg_receiver.recv().unwrap() {
WaitingServerMsg::MsgReceived => client.server_message_recv.recv().unwrap(),
WaitingServerMsg::UserCanceled => client.keyboard_input_recv.recv().unwrap(),
}
response
}
enum PromptedRoleResponse {
@ -292,9 +459,13 @@ fn prompt_user_for_role(client: &Client, stream: &mut UnixStream) -> PromptedRol
} else {
prompt = String::from("Do you want to play as Black (B) or Spectate (S)?");
}
println!("{}", prompt);
let mut input = String::from(client.keyboard_input_recv.recv().unwrap().trim());
if input.trim() == EXIT_MSG {
client
.display_sender
.send(DisplayMessage::Information(prompt))
.unwrap();
let mut input = client.keyboard_input_recv.recv().unwrap();
input = String::from(input.trim());
if input == EXIT_MSG {
return PromptedRoleResponse::Exit;
}
@ -305,13 +476,21 @@ fn prompt_user_for_role(client: &Client, stream: &mut UnixStream) -> PromptedRol
match clichess::parse_to_role(&input) {
Ok(r) => {
if !available_roles.contains(&r) {
println!("Sorry, this side is not available.");
client
.display_sender
.send(DisplayMessage::Message(String::from(
"Sorry, this side is not available.",
)))
.unwrap();
return PromptedRoleResponse::Retry;
}
return PromptedRoleResponse::Role(r);
}
Err(e) => {
println!("{}", e);
client
.display_sender
.send(DisplayMessage::Message(format!("{}", e)))
.unwrap();
return PromptedRoleResponse::Retry;
}
};

View File

@ -89,6 +89,7 @@ fn handle_player(
Ok(ClientRequest::WaitForNextMove) => {
//return true if user exited.
if wait_for_next_move(&mut stream, server) {
send_ok(&mut stream);
break;
}
}
@ -207,7 +208,10 @@ fn wait_for_opponent_move(server: &Server) -> Result<GameInfo, RecvPositionError
Err(TryRecvError::Empty) => { /*nothing to do*/ }
}
match server.client_message_recv.try_recv() {
Ok(_) => returned_result = Err(RecvPositionError::UserCanceledError),
Ok(_) => {
println!("User exited.");
returned_result = Err(RecvPositionError::UserCanceledError);
}
Err(TryRecvError::Disconnected) => println!("Error: client disconnected."),
Err(TryRecvError::Empty) => { /*nothing to do*/ }
}

View File

@ -1,4 +1,3 @@
use colored::Colorize;
use serde::{Deserialize, Serialize};
use shakmaty::fen::Fen;
use shakmaty::san::ParseSanError;
@ -11,6 +10,7 @@ use std::io;
use std::io::prelude::*;
use std::io::{BufReader, Write};
use std::os::unix::net::UnixStream;
use termion::raw::RawTerminal;
//message to send to the server to signal we disconnected.
pub const EXIT_MSG: &str = "exit";
@ -129,25 +129,26 @@ impl fmt::Display for MoveInputError {
}
}
pub fn board_representation(chess: &Chess, side: Color) -> String {
let mut s = String::from("");
pub fn print_board_representation(
chess: &Chess,
side: Color,
stdout: &mut RawTerminal<io::StdoutLock>,
) {
let mut rank_numbers;
if side == Color::White {
rank_numbers = 8;
} else {
rank_numbers = 1;
}
let mut linenb = 6;
for v in board_string_representation(chess, side) {
write!(stdout, "{}", termion::cursor::Goto(10, linenb)).unwrap();
for square_to_print in v {
s.push_str(&format!(
"{}",
square_to_print
.square_representation
.color(square_to_print.color)
.on_color(square_to_print.background_color)
));
print_square(&square_to_print, stdout);
}
s.push_str(&format!("{}\n\r", rank_numbers));
linenb += 1;
resetcolors(stdout);
write!(stdout, "{}", rank_numbers).unwrap();
if side == Color::White {
rank_numbers -= 1;
} else {
@ -158,11 +159,35 @@ pub fn board_representation(chess: &Chess, side: Color) -> String {
if side == Color::Black {
files.reverse();
}
resetcolors(stdout);
write!(stdout, "{}", termion::cursor::Goto(10, linenb)).unwrap();
for file in files {
s.push_str(&format!("{} ", file));
write!(stdout, "{} ", file).unwrap();
}
s.push_str("\n\r");
s
write!(stdout, "{}", termion::cursor::Goto(5, 15)).unwrap();
stdout.flush().unwrap();
}
fn print_square(square_to_print: &SquareToPrint, stdout: &mut RawTerminal<io::StdoutLock>) {
match square_to_print.color.as_ref() {
"black" => write!(stdout, "{}", termion::color::Fg(termion::color::Black)),
_ => write!(stdout, "{}", termion::color::Fg(termion::color::Reset)),
}
.unwrap();
match square_to_print.background_color.as_ref() {
"white" => write!(stdout, "{}", termion::color::Bg(termion::color::White)),
"green" => write!(stdout, "{}", termion::color::Bg(termion::color::Green)),
_ => write!(stdout, "{}", termion::color::Bg(termion::color::Reset)),
}
.unwrap();
write!(stdout, "{}", square_to_print.square_representation).unwrap();
}
fn resetcolors(stdout: &mut RawTerminal<io::StdoutLock>) {
write!(stdout, "{}", termion::color::Fg(termion::color::Reset)).unwrap();
write!(stdout, "{}", termion::color::Bg(termion::color::Reset)).unwrap();
}
fn board_string_representation(chess: &Chess, side: Color) -> Vec<Vec<SquareToPrint>> {