extern crate argparse; extern crate chrono; mod consulter; mod game; mod gui; mod parser; use crate::consulter::*; use crate::game::*; use crate::gui::*; use crate::parser::*; use argparse::*; use chrono::Local; use std::env; use std::time::SystemTime; enum InputSource { Player, Log, } struct GUI { screen: Window, game_log: GameLog, message_log: MessageLog, action_entry: ActionEntry, board_gui: BoardGUI, input_source: InputSource, } impl GUI { fn write_header(&self, header: &str) { self.screen.mv(0, 0); self.screen.clrtoeol(); self.screen.addstr(format!( "takwrap {} | to switch GUI element | {}", env!("CARGO_PKG_VERSION").to_string(), header )); self.screen.mvchgat(0, 0, -1, A_REVERSE, -1); self.screen.refresh(); } fn new(size: u8) -> GUI { let screen = initscr(); cbreak(); noecho(); curs_set(2); let has_colours = has_colors(); let (black_colour, white_colour, red_colour): (u8, u8, u8) = (1, 2, 3); if has_colours { start_color(); use_default_colors(); init_pair(black_colour as i16, -1, COLOR_RED); init_pair(white_colour as i16, -1, COLOR_BLUE); init_pair(red_colour as i16, COLOR_RED, -1); } let voff = 2; // let width = screen.get_max_x(); let height = screen.get_max_y() - voff; screen.refresh(); let cell_height = 4; let cell_width = 8; let hoff = 3; let board_bottom = voff + (size * cell_height + 3) as i32; let board_right = (hoff + size * cell_width + 3) as i32; let board_gui = BoardGUI::new( voff, 0, size, cell_height, cell_width, hoff, has_colours, white_colour, black_colour, ); let game_log = GameLog::new( voff, board_right, height - 2, 30, has_colours, white_colour, black_colour, ); // Need room for '8___011111111', for example let action_entry = ActionEntry::new(board_bottom, 0, 13); let message_log = MessageLog::new( board_bottom + 2, 0, height - board_bottom - 1, board_right, has_colours, red_colour, ); GUI { screen, game_log, message_log, action_entry, board_gui, input_source: InputSource::Player, } } fn query_input( &mut self, game: &Game, call_proc: bool, p1_white: bool, engine: &str, ) -> Result { match self.input_source { InputSource::Player => { let player = game.query_current_player(); let inp = match player { Player::Black => { if call_proc && p1_white { self.message_log .log_message(String::from("Opponent is thinking...")); consult_next_action(&game, &engine) } else { Ok(self.action_entry.read_action()) } } Player::White => { if call_proc && (!p1_white) { self.message_log .log_message(String::from("Opponent is thinking...")); consult_next_action(&game, &engine) } else { Ok(self.action_entry.read_action()) } } }; match inp { Err(e) => Err(format!("Error: {}", e)), Ok(ok) => Ok(ok), } } InputSource::Log => Ok(self.game_log.read_action()), } } fn handle_input( &mut self, game: &mut Game, call_proc: bool, p1_white: bool, input: Result, ) -> bool { let player = game.query_current_player(); let engine_turn = call_proc && ((p1_white && player == Player::Black) || ((!p1_white) && player == Player::White)); match input { Err(e) => { if engine_turn { self.message_log .log_error(format!("Unrecoverable error: {}", e)); return false; } } Ok(gui_result) => match gui_result { GUIResult::Quit => { self.message_log .log_message(String::from("Press any key to quit.")); return false; } GUIResult::SwitchElement => match self.input_source { InputSource::Player => { self.input_source = InputSource::Log; self.write_header(LOG_HEADER); } InputSource::Log => { self.input_source = InputSource::Player; self.write_header(ACTION_HEADER); } }, GUIResult::Undo => match game.undo() { Ok(pos_vec) => { self.message_log .log_message(String::from("Game undone by one turn.")); self.board_gui.update(&game, pos_vec); self.game_log.update(&game); } Err(err) => { self.message_log.log_error(err); } }, GUIResult::Raw(raw) => { let stone_owner = game.query_stone_owner(); let act = parse_action(stone_owner, &raw); match act { Err(err) => { self.message_log .log_error(format!("Input \"{}\": {}", raw, err)); if engine_turn { return false; } } Ok(act) => { let pn = game.query_current_player_name(); self.message_log.log_message(format!( "{} performs {}{}", pn, act, if stone_owner != player { format!(" with a {} stone.", stone_owner) } else { String::from(".") } )); let act_string = act.to_string(); match game.perform_action(act) { Err(e) => { if engine_turn { self.message_log.log_error(String::from( "Error: external and internal game states disagree. Press any key to quit.", )); return false; } else { self.message_log.log_error(format!( "Cannot perform \"{}\": {}", act_string, e )); } } Ok((pos_vec, win)) => { self.board_gui.update(&game, pos_vec); self.game_log.update(&game); if let Some(w) = win { self.message_log.log_message(format!( "Game over: {}\nPress any key to quit.", w )); return false; } } } } } } }, } return true; } } const ACTION_HEADER: &str = "Enter action in PTN"; const LOG_HEADER: &str = "U to undo a turn | / to scroll | Q to quit"; fn main() { let mut p1_name = match env::var("USER") { Err(_) => String::from("Player1"), Ok(user) => user, }; let mut p2_name = String::from("Player2"); let mut engine = String::new(); let mut name = String::new(); let mut size: u8 = 5; { let mut ap = ArgumentParser::new(); ap.set_description("Test description."); ap.refer(&mut p1_name).add_option( &["--player1"], Store, "Name of first player, defaults to $USER. See --name-white below for Black/White assignment.", ); ap.refer(&mut p2_name).add_option( &["--player2"], Store, "Name of second player, defaults to ``Player2'' and is overridden by the arument ENGINE of --engine when applicable.", ); ap.refer(&mut engine).add_option( &["-e", "--engine"], Store, "Name of process which will be used as a computer opponent. The process must accept a single argument point to a PTN file of the current game state, and must generate a single PTN action on STDOUT. The engine will replace the second player, and the name of player 2 is set to this argument.", ); ap.refer(&mut name).add_option( &["--name-white"], Store, "Force the player named NAME to play white. If not supplied, assignment is ``random''.", ).metavar("NAME"); ap.refer(&mut size).add_option( &["-s", "--size"], Store, "Size of board (one dimension), default is 5.", ); ap.add_option( &["-V", "--version"], Print(env!("CARGO_PKG_VERSION").to_string()), "Show version", ); ap.parse_args_or_exit(); } let call_proc; if !engine.is_empty() { p2_name = engine.clone(); call_proc = true; // Perhaps there's a better way. } else { call_proc = false; } let mut gui = GUI::new(size); let p1_white; if !name.is_empty() { if name == p1_name { p1_white = true; } else if name == p2_name { p1_white = false; } else { panic!( "Player named as white ({}) is not a player in this game ({} and {}).", name, p1_name, p2_name ); } } else { p1_white = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { Ok(n) => n.as_millis() % 2 == 0, Err(_) => false, }; } let date_string = Local::now().to_string(); let (mut game, warning) = if p1_white { Game::new(size, &p1_name, &p2_name, &date_string) } else { Game::new(size, &p2_name, &p1_name, &date_string) }; gui.message_log.log_error(warning); gui.game_log.update(&game); gui.write_header(ACTION_HEADER); loop { let inp = gui.query_input(&game, call_proc, p1_white, &engine); if !gui.handle_input(&mut game, call_proc, p1_white, inp) { gui.screen.getch(); break; } } // board_gui.read_action(); endwin(); }