634 lines
22 KiB
Rust
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(¤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<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(
|
|
¤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<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()
|
|
}
|