extern crate termion; use clichess::{ClientRequest, GameInfo, RecvPositionError, UserRole, EXIT_MSG}; use shakmaty::{fen, Chess, Color, Outcome, Position, Setup}; use std::io; use std::os::unix::net::UnixStream; use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::thread; use std::time; use termion::event::{parse_event, Event, Key}; use std::io::{stdin, stdout, Read, Write}; use termion::raw::{IntoRawMode, RawTerminal}; struct Client { player: clichess::Player, side: Color, display_sender: Sender, waiting_server_msg_receiver: Receiver, keyboard_input_recv: Receiver, server_message_recv: Receiver, opponent_name: Option, last_move: Option, is_player_turn: Arc>, } fn main() { let version = env!("CARGO_PKG_VERSION"); println!("Running clichess version {}", version); let username = std::env::args().nth(1).expect("no name given"); let public_key = std::env::args().nth(2).expect("no public key given"); //send username and public key to server let mut stream = match UnixStream::connect("/tmp/clichess.socket") { Ok(sock) => sock, Err(_) => { println!("clichess daemon is not running."); return; } }; let is_player_turn = Arc::new(Mutex::new(true)); 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(), is_player_turn.clone(), ); start_display_thread(display_receiver); //start a thread to listen to server messages let server_message_recv = setup_server_message_recv(&stream, waiting_server_msg_sender.clone()).unwrap(); let mut client = Client { player: clichess::Player { role: UserRole::Spectator, username, public_key, }, side: Color::White, display_sender: display_sender.clone(), waiting_server_msg_receiver, keyboard_input_recv, server_message_recv, opponent_name: Option::None, last_move: Option::None, is_player_turn, }; let login_result = login(&mut client, &mut stream); if login_result == LoginResult::UserExited { send_request(&client, &mut stream, ClientRequest::Exit); client.display_sender.send(DisplayMessage::Exit).unwrap(); return; } client.display_sender.send(DisplayMessage::Clear).unwrap(); if client.player.role == UserRole::Black { client.side = Color::Black; } if client.player.role == UserRole::Spectator { client .display_sender .send(DisplayMessage::Information(format!( "Hello, {} !\n\r You're spectating !", client.player.username ))) .unwrap(); } else { 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(); } loop { client.display_sender.send(DisplayMessage::Help).unwrap(); if !game_loop(&mut client, &mut stream) { break; } client.display_sender.send(DisplayMessage::Clear).unwrap(); } client.display_sender.send(DisplayMessage::Clear).unwrap(); send_request(&client, &mut stream, ClientRequest::Exit); client.display_sender.send(DisplayMessage::Exit).unwrap(); } #[derive(PartialEq)] enum LoginResult { Success, UserExited, } fn login(client: &mut Client, stream: &mut UnixStream) -> LoginResult { let mut response = String::from("KO"); while &response != "OK" { match prompt_user_for_role(client, stream) { PromptedRoleResponse::Role(role) => { client.player.role = role.clone(); response = send_request( client, stream, ClientRequest::Login { username: client.player.username.clone(), pubkey: client.player.public_key.clone(), role, }, ); } PromptedRoleResponse::Exit => return LoginResult::UserExited, PromptedRoleResponse::Retry => continue, }; } LoginResult::Success } fn game_loop(client: &mut Client, stream: &mut UnixStream) -> bool { let mut replay = false; let mut current_position = fetch_initial_chess_position(client, stream); loop { let is_player_turn_loop = clichess::is_player_turn(&client.player, current_position.turn()); { let mut is_player_turn = client.is_player_turn.lock().unwrap(); *is_player_turn = is_player_turn_loop; } if is_player_turn_loop || client.player.role == UserRole::Spectator { let mut message = String::from(""); let opponent_name = client.opponent_name.clone(); if opponent_name.is_some() { message.push_str(&format!("{} played", opponent_name.expect("is some"))); client .last_move .clone() .map(|chessmove| message.push_str(&format!(" {}", chessmove))); } if message.len() > 0 { client .display_sender .send(DisplayMessage::Information(message)) .unwrap(); } } client .display_sender .send(DisplayMessage::Chessboard { fen: fen::fen(¤t_position), side: client.side.clone(), }) .unwrap(); //check if game is over. if current_position.is_game_over() { break; } if is_player_turn_loop { //it's the user turn, taking user input 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, stream, ClientRequest::Play(input.clone())); if response == String::from("KO") { client .display_sender .send(DisplayMessage::Message(format!("Invalid move: {}", input))) .unwrap(); //go back to taking user input continue; } client.last_move = Some(input); match get_current_position(client, stream) { Ok(position) => current_position = position, Err(_) => break, }; //clear message client .display_sender .send(DisplayMessage::Message(String::default())) .unwrap(); } else { match wait_for_next_move(client, stream) { Ok(position) => current_position = position, Err(_) => break, }; } } let end_message = match current_position.outcome() { None => "", Some(Outcome::Draw) => "Draw game.", Some(Outcome::Decisive { winner }) => { if winner == Color::White { "White has won the game." } else { "Black has won the game." } } } .to_string(); if !end_message.is_empty() { //accept input if client.player.role != UserRole::Spectator { client .display_sender .send(DisplayMessage::Message(format!( "{}\n\rPress enter to rematch.", end_message ))) .unwrap(); let mut is_player_turn = client.is_player_turn.lock().unwrap(); *is_player_turn = true; } else { client .display_sender .send(DisplayMessage::Message(format!("{}", end_message))) .unwrap(); thread::sleep(time::Duration::from_secs(5)); return false; } let buffer = client.keyboard_input_recv.recv().unwrap(); if buffer == EXIT_MSG { client.waiting_server_msg_receiver.recv().unwrap(); } else { replay = true; client .display_sender .send(DisplayMessage::Message(format!( "{}\n\rWaiting for your opponent to restart...", end_message ))) .unwrap(); send_request(client, stream, ClientRequest::Reset); } } replay } fn send_request(client: &Client, stream: &mut UnixStream, request: ClientRequest) -> String { let response: String; match serde_json::to_string(&request) { Ok(request_str) => { clichess::write_to_stream(stream, request_str).unwrap(); response = fetch_message_from_server(client); } Err(e) => { client .display_sender .send(DisplayMessage::Message(format!( "Error when parsing client request: {}", e ))) .unwrap(); response = String::from("KO"); } }; response } #[derive(PartialEq, Clone)] pub enum DisplayMessage { Chessboard { fen: String, side: Color }, Message(String), Information(String), Debug(String), Help, Input(Key), RemoveLastInput, SwitchAsciiMode, Enter, Clear, Exit, } fn start_display_thread(request_recv: Receiver) { 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(); let mut ascii_mode = false; let mut last_fen_position = String::default(); let mut last_side = Color::White; loop { let msg = request_recv.recv().unwrap(); match msg { DisplayMessage::Chessboard { fen, side } => { let current_position = clichess::parse_position(&fen); last_fen_position = fen.clone(); last_side = side.clone(); clichess::print_board_representation( ¤t_position, side, &mut stdout, ascii_mode, ); } DisplayMessage::SwitchAsciiMode => { ascii_mode = !ascii_mode; write!(stdout, "{}", termion::cursor::Save,).unwrap(); clichess::print_board_representation( &clichess::parse_position(&last_fen_position), last_side, &mut stdout, ascii_mode, ); write!(stdout, "{}", termion::cursor::Restore).unwrap(); } DisplayMessage::Message(s) => write!( stdout, "{}{}{}{}{}", termion::cursor::Save, termion::cursor::Goto(3, 18), 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::RemoveLastInput => write!( stdout, "{}{}", termion::cursor::Left(1), termion::clear::UntilNewline ) .unwrap(), DisplayMessage::Help => print_help(&mut stdout), DisplayMessage::Debug(s) => write!( stdout, "{}{}{}{}{}", termion::cursor::Save, termion::cursor::Goto(3, 30), termion::clear::CurrentLine, s, termion::cursor::Restore ) .unwrap(), DisplayMessage::Exit => break, } stdout.flush().unwrap(); } write!(stdout, "{}", termion::clear::All,).unwrap(); stdout.flush().unwrap(); }); } fn print_help(stdout: &mut RawTerminal) { write!(stdout, "{}", termion::cursor::Save).unwrap(); let help = [ "move are parsed using SAN (Nc3) or UCI (b1c3)", "@ to toggle ascii mode", "Ctrl-C to quit", ]; for (i, h) in help.iter().enumerate() { write!(stdout, "{}{}", termion::cursor::Goto(3, 22 + (i as u16)), h).unwrap(); } write!(stdout, "{}", termion::cursor::Restore).unwrap(); } fn start_keyboard_input_thread( display_sender: Sender, waiting_server_msg_sender: Sender, is_player_turn: Arc>, ) -> Receiver { 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 b = bytes.next().unwrap().unwrap(); let e = parse_event(b, &mut bytes).unwrap(); let is_player_turn = is_player_turn.lock().unwrap(); if !*is_player_turn && e != Event::Key(Key::Ctrl('c')) && e != Event::Key(Key::Char('@')) { //ignore input when it's not the client turn continue; } match e { Event::Key(Key::Ctrl('c')) => { sender.send(String::from(EXIT_MSG)).unwrap(); waiting_server_msg_sender .send(WaitingServerMsg::UserCanceled) .unwrap(); break; } Event::Key(Key::Char('\n')) => { 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(); } Event::Key(Key::Char('@')) => { display_sender .send(DisplayMessage::SwitchAsciiMode) .unwrap(); } Event::Key(Key::Char(c)) => { if buffer.len() < 10 { buffer.push_str(&c.to_string()); display_sender .send(DisplayMessage::Input(Key::Char(c))) .unwrap(); } } Event::Key(Key::Backspace) => { if buffer.len() > 0 { buffer.pop(); display_sender .send(DisplayMessage::RemoveLastInput) .unwrap(); } } Event::Key(_) => continue, Event::Mouse(_) => continue, Event::Unsupported(_) => continue, }; } }); receiver } fn display_key(stdout: &mut RawTerminal, 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, ) -> io::Result> { let (sender, receiver) = channel(); let thread_stream = stream.try_clone()?; thread::spawn(move || { loop { //wait for server message 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) } //get current position from server fn get_current_position( client: &mut Client, stream: &mut UnixStream, ) -> Result { let response = send_request(client, stream, ClientRequest::GetGameInfo); if response.is_empty() { Err(RecvPositionError::UserCanceledError) } else { let game_info: GameInfo = serde_json::from_str(&response).unwrap(); update_client(&game_info, client); Ok(clichess::parse_position(&game_info.game_fen)) } } fn wait_for_next_move( client: &mut Client, stream: &mut UnixStream, ) -> Result { client .display_sender .send(DisplayMessage::Information(String::from( "Waiting for opponent move...", ))) .unwrap(); send_request(client, stream, ClientRequest::WaitForNextMove); 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(); update_client(&game_info, client); Ok(clichess::parse_position(&game_info.game_fen)) } } fn update_client(game_info: &GameInfo, client: &mut Client) { if game_info.opponent_name != "" { client.opponent_name = Some(game_info.opponent_name.clone()); } if game_info.last_move != "" { client.last_move = Some(game_info.last_move.clone()); } } fn fetch_message_from_server(client: &Client) -> String { match client.waiting_server_msg_receiver.recv().unwrap() { WaitingServerMsg::MsgReceived => client.server_message_recv.recv().unwrap(), WaitingServerMsg::UserCanceled => client.keyboard_input_recv.recv().unwrap(), } } enum PromptedRoleResponse { Role(UserRole), Exit, Retry, } fn prompt_user_for_role(client: &Client, stream: &mut UnixStream) -> PromptedRoleResponse { let response = send_request(client, stream, ClientRequest::FetchAvailableRoles); let available_roles = roles_from_str(&response).unwrap(); let mut prompt = String::new(); if !available_roles.contains(&UserRole::White) && !available_roles.contains(&UserRole::Black) { prompt.push_str("You can only spectate this game. Press enter to start spectating."); } else if available_roles.contains(&UserRole::White) { prompt = String::from("Do you want to play as White (W)"); if available_roles.contains(&UserRole::Black) { prompt.push_str(", Black (B)"); } prompt.push_str(" or Spectate (S)?"); } else { prompt = String::from("Do you want to play as Black (B) or Spectate (S)?"); } 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 { client.waiting_server_msg_receiver.recv().unwrap(); return PromptedRoleResponse::Exit; } if !available_roles.contains(&UserRole::White) && !available_roles.contains(&UserRole::Black) { //we can only spectate input = String::from("S"); } match clichess::parse_to_role(&input) { Ok(r) => { if !available_roles.contains(&r) { client .display_sender .send(DisplayMessage::Message(String::from( "Sorry, this side is not available.", ))) .unwrap(); return PromptedRoleResponse::Retry; } return PromptedRoleResponse::Role(r); } Err(e) => { client .display_sender .send(DisplayMessage::Message(format!("{}", e))) .unwrap(); return PromptedRoleResponse::Retry; } }; } fn fetch_initial_chess_position(client: &Client, stream: &mut UnixStream) -> Chess { let response = send_request(client, stream, ClientRequest::GetGameInfo); let game_info: GameInfo = serde_json::from_str(&response).unwrap(); clichess::parse_position(&game_info.game_fen) } fn roles_from_str(s: &str) -> Result, String> { s.split(',').map(clichess::parse_to_role).collect() }