clichess/src/bin/client.rs
Artlef ae3791a24b Reset the board at the end of the game
The two players must press enter to reset. For spectators, the game is
stopped.
2020-12-06 22:47:02 +01:00

634 lines
22 KiB
Rust

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<DisplayMessage>,
waiting_server_msg_receiver: Receiver<WaitingServerMsg>,
keyboard_input_recv: Receiver<String>,
server_message_recv: Receiver<String>,
opponent_name: Option<String>,
last_move: Option<String>,
is_player_turn: Arc<Mutex<bool>>,
}
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(&current_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<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();
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(
&current_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<io::StdoutLock>) {
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<DisplayMessage>,
waiting_server_msg_sender: Sender<WaitingServerMsg>,
is_player_turn: Arc<Mutex<bool>>,
) -> 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 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<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()?;
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<Chess, RecvPositionError> {
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<Chess, RecvPositionError> {
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<Vec<UserRole>, String> {
s.split(',').map(clichess::parse_to_role).collect()
}