summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/game.rs46
-rw-r--r--src/gui.rs68
-rw-r--r--src/main.rs423
3 files changed, 357 insertions, 180 deletions
diff --git a/src/game.rs b/src/game.rs
index 5db2c48..4a80ce7 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -743,23 +743,28 @@ impl Game {
self.size
}
- pub fn query_action_lines(&self) -> String {
- let mut result = String::new();
+ pub fn query_action_lines(&self) -> Vec<String> {
+ let mut result = Vec::new();
let mut newline = true;
let num_actions = self.actions.len();
if num_actions > 0 {
- result += "1. ";
+ let mut line = String::new();
+ line += "1. ";
for i in 0..num_actions {
if i > 1 && newline {
// TODO: Is placing the opponent's first stone the zeroeth action?
- result += &format!("{}. ", i / 2 + 1);
+ line += &format!("{}. ", i / 2 + 1);
}
- result += &format!("{} ", self.actions[i]);
+ line += &format!("{} ", self.actions[i].to_string());
if i > 0 && !newline {
- result.push('\n');
+ result.push(line);
+ line = String::new();
}
newline = !newline;
}
+ if !line.is_empty() {
+ result.push(line);
+ }
}
result
}
@@ -834,6 +839,33 @@ impl Game {
}
}
}
+
+ pub fn undo(&mut self) -> Result<Vec<Position>, String> {
+ if self.actions.len() > 0 {
+ self.actions.pop();
+ self.states.pop();
+ self.turn_order = match self.actions.len() {
+ 0 => TurnOrder::WhitePlacesBlack,
+ 1 => TurnOrder::BlackPlacesWhite,
+ _ => TurnOrder::Normal,
+ };
+ self.current_player = match self.current_player {
+ Player::Black => Player::White,
+ Player::White => Player::Black,
+ };
+ // I'm too lazy to work out exactly which squares must be
+ // redrawn when an action is undone
+ let mut redraw_all: Vec<Position> = Vec::new();
+ for x in 0..self.size {
+ for y in 0..self.size {
+ redraw_all.push(Position { x, y });
+ }
+ }
+ Ok(redraw_all)
+ } else {
+ Err(String::from("Cannot undo in an empty game!"))
+ }
+ }
}
impl fmt::Display for Game {
@@ -845,7 +877,7 @@ impl fmt::Display for Game {
self.white_player_name,
self.black_player_name,
self.size,
- self.query_action_lines()
+ self.query_action_lines().join("\n")
)
}
}
diff --git a/src/gui.rs b/src/gui.rs
index 95bc8ee..fb32508 100644
--- a/src/gui.rs
+++ b/src/gui.rs
@@ -8,6 +8,7 @@ use crate::game::*;
#[derive(PartialEq)]
pub enum GUIResult {
Undo,
+ Quit,
SwitchElement,
Raw(String),
}
@@ -18,6 +19,9 @@ pub struct GameLog {
has_colours: bool,
white_colour: u8,
black_colour: u8,
+ num_lines: usize,
+ offset: usize,
+ action_lines: Vec<String>,
}
impl GameLog {
@@ -33,17 +37,20 @@ impl GameLog {
let gl = GameLog {
pieces_window: newwin(4, width, ypos, xpos),
action_window: newwin(height - 5, width, ypos + 5, xpos),
- has_colours: has_colours,
- white_colour: white_colour,
- black_colour: black_colour,
+ has_colours,
+ white_colour,
+ black_colour,
+ num_lines: (height - 7) as usize,
+ offset: 0,
+ action_lines: Vec::new(),
};
- gl.action_window.setscrreg(0, height);
- gl.action_window.scrollok(true);
+ gl.action_window.keypad(true);
+ gl.action_window.nodelay(false);
gl
}
- pub fn update(&self, game: &Game) {
+ pub fn update(&mut self, game: &Game) {
self.pieces_window.clear();
if self.has_colours {
@@ -104,12 +111,55 @@ impl GameLog {
}
self.pieces_window.addstr(game.query_current_player_name());
+ let new_lines = game.query_action_lines();
+ if new_lines.len() > self.num_lines {
+ self.offset = new_lines.len() - self.num_lines;
+ } else {
+ self.offset = 0;
+ }
+ self.action_lines = new_lines.clone();
+ self.list_actions();
+
self.pieces_window.refresh();
+ }
- // Perhaps it's best to avoid clearing things? Overwrite the specific characters instead?
+ fn list_actions(&self) {
self.action_window.clear();
- self.action_window.addstr(game.query_action_lines());
+ self.action_window.draw_box(0, 0);
+ let mut y = 1;
+ for i in 0..self.num_lines {
+ if let Some(line) = self.action_lines.get(i + self.offset) {
+ self.action_window.mv(y, 2);
+ self.action_window.addstr(line);
+ y += 1;
+ }
+ }
self.action_window.refresh();
+ self.action_window.mv(1, 2);
+ }
+
+ pub fn read_action(&mut self) -> GUIResult {
+ loop {
+ self.list_actions();
+ if let Some(inp) = self.action_window.getch() {
+ match inp {
+ KeyUp => {
+ if self.offset > 0 {
+ self.offset -= 1;
+ }
+ }
+ KeyDown => {
+ if self.offset + self.num_lines < self.action_lines.len() {
+ self.offset += 1;
+ }
+ }
+ Character('\t') => return GUIResult::SwitchElement,
+ Character('U') => return GUIResult::Undo,
+ Character('Q') => return GUIResult::Quit,
+ _ => (),
+ }
+ }
+ }
}
}
@@ -389,7 +439,7 @@ impl ActionEntry {
self.window.addch('_');
self.window.mv(y, x - 1);
} else {
- if result.len() < self.max_len {
+ if (result.len() < self.max_len) && (ascii > 32) && (ascii < 127) {
result.push(c);
self.window.addch(c);
}
diff --git a/src/main.rs b/src/main.rs
index 5fe8c18..e5ba074 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,98 +16,283 @@ use chrono::Local;
use std::env;
use std::time::SystemTime;
+enum InputSource {
+ Player,
+ Log,
+}
+
struct GUI {
screen: Window,
- // width: i32,
- // height: i32,
game_log: GameLog,
message_log: MessageLog,
action_entry: ActionEntry,
board_gui: BoardGUI,
+ input_source: InputSource,
}
-fn init_gui(size: u8) -> GUI {
- let screen = initscr();
- cbreak();
- noecho();
- curs_set(2);
+impl GUI {
+ fn write_header(&self, header: &str) {
+ self.screen.mv(0, 0);
+ self.screen.clrtoeol();
+ self.screen.addstr(format!(
+ "takwrap {} | <TAB> to switch GUI element | {}",
+ env!("CARGO_PKG_VERSION").to_string(),
+ header
+ ));
+ self.screen.mvchgat(0, 0, -1, A_REVERSE, -1);
+ self.screen.refresh();
+ }
- let has_colours = has_colors();
- let (black_colour, white_colour, red_colour): (u8, u8, u8) = (1, 2, 3);
+ fn new(size: u8) -> GUI {
+ let screen = initscr();
+ cbreak();
+ noecho();
+ curs_set(2);
- 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 has_colours = has_colors();
+ let (black_colour, white_colour, red_colour): (u8, u8, u8) = (1, 2, 3);
- screen.addstr(format!(
- "takwrap {} | <TAB> to switch GUI element",
- env!("CARGO_PKG_VERSION").to_string()
- ));
+ 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);
+ }
- screen.mvchgat(0, 0, -1, A_REVERSE, -1);
- let voff = 2;
+ let voff = 2;
- let width = screen.get_max_x();
- let height = screen.get_max_y() - voff;
+ // let width = screen.get_max_x();
+ let height = screen.get_max_y() - voff;
- screen.refresh();
+ 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 cell_height = 4;
- let cell_width = 8;
- let hoff = 3;
+ let game_log = GameLog::new(
+ voff,
+ board_right,
+ height - 2,
+ 30,
+ has_colours,
+ white_colour,
+ black_colour,
+ );
- let board_bottom = voff + (size * cell_height + 3) as i32;
- let board_right = (hoff + size * cell_width + 3) as i32;
+ // Need room for '8___011111111', for example
+ let action_entry = ActionEntry::new(board_bottom, 0, 13);
- let board_gui = BoardGUI::new(
- voff,
- 0,
- size,
- cell_height,
- cell_width,
- hoff,
- has_colours,
- white_colour,
- black_colour,
- );
+ let message_log = MessageLog::new(
+ board_bottom + 2,
+ 0,
+ height - board_bottom - 1,
+ board_right,
+ has_colours,
+ red_colour,
+ );
- let game_log = GameLog::new(
- voff,
- board_right,
- height,
- width - board_right,
- has_colours,
- white_colour,
- black_colour,
- );
+ GUI {
+ screen,
+ game_log,
+ message_log,
+ action_entry,
+ board_gui,
+ input_source: InputSource::Player,
+ }
+ }
- // Need room for '8___011111111', for example
- let action_entry = ActionEntry::new(board_bottom, 0, 13);
+ fn query_input(
+ &mut self,
+ game: &Game,
+ call_proc: bool,
+ p1_white: bool,
+ engine: &str,
+ ) -> Result<GUIResult, String> {
+ match self.input_source {
+ InputSource::Player => {
+ let player = game.query_current_player();
- let message_log = MessageLog::new(
- board_bottom + 2,
- 0,
- height - board_bottom - 1,
- board_right,
- has_colours,
- red_colour,
- );
+ 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())
+ }
+ }
+ };
- GUI {
- screen,
- // width,
- // height,
- game_log,
- message_log,
- action_entry,
- board_gui,
+ 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<GUIResult, String>,
+ ) -> 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 => {
+ if engine_turn {
+ self.message_log.log_error(String::from(
+ "Internal error: engine passed quit message somehow. Press any key to quit.",
+ ));
+ } else {
+ self.message_log
+ .log_message(String::from("Press any key to quit."));
+ }
+ self.screen.getch();
+ return false;
+ }
+ GUIResult::SwitchElement => {
+ if engine_turn {
+ self.message_log.log_error(String::from(
+ "Internal error: engine passed switch-element message somehow. Press any key to quit.",
+ ));
+ self.screen.getch();
+ return false;
+ } else {
+ 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 => {
+ if engine_turn {
+ self.message_log.log_error(String::from(
+ "Internal error: engine passed undo message somehow. Press any key to quit.",
+ ));
+ self.screen.getch();
+ return false;
+ } else {
+ 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.",
+ ));
+ self.screen.getch();
+ 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!("{}", w));
+ return false;
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ }
+ return true;
}
}
+const ACTION_HEADER: &str = "Enter action in PTN";
+const LOG_HEADER: &str = "U to undo a turn | <UP>/<DOWN> to scroll | Q to quit";
+
fn main() {
let mut p1_name = match env::var("USER") {
Err(_) => String::from("Player1"),
@@ -174,7 +359,7 @@ fn main() {
call_proc = false;
}
- let gui = init_gui(size);
+ let mut gui = GUI::new(size);
let p1_white;
if !name.is_empty() {
@@ -204,105 +389,15 @@ fn main() {
gui.message_log.log_error(warning);
gui.game_log.update(&game);
- loop {
- let player = game.query_current_player();
- let engine_turn = call_proc
- && ((p1_white && player == Player::Black) || ((!p1_white) && player == Player::White));
-
- let result = match player {
- Player::Black => {
- if call_proc && p1_white {
- gui.message_log
- .log_message(String::from("Opponent is thinking..."));
- consult_next_action(&game, &engine)
- } else {
- Ok(gui.action_entry.read_action())
- }
- }
- Player::White => {
- if call_proc && (!p1_white) {
- gui.message_log
- .log_message(String::from("Opponent is thinking..."));
- consult_next_action(&game, &engine)
- } else {
- Ok(gui.action_entry.read_action())
- }
- }
- };
+ gui.write_header(ACTION_HEADER);
- match result {
- Err(e) => {
- gui.message_log.log_error(format!("Engine error: {}", e));
- break;
- }
- Ok(gui_result) => match gui_result {
- GUIResult::SwitchElement => {
- if engine_turn {
- gui.message_log.log_error(String::from(
- "Internal error: engine passed switch-element message somehow.",
- ));
- break;
- }
- }
- GUIResult::Undo => {
- if engine_turn {
- gui.message_log.log_error(String::from(
- "Internal error: engine passed undo message somehow.",
- ));
- break;
- }
- }
- GUIResult::Raw(raw) => {
- let stone_owner = game.query_stone_owner();
- let act = parse_action(stone_owner, &raw);
- match act {
- Err(err) => {
- gui.message_log
- .log_error(format!("Input \"{}\": {}", raw, err));
- if engine_turn {
- break;
- }
- }
- Ok(act) => {
- let pn = game.query_current_player_name();
- gui.message_log.log_message(format!(
- "{} performs {}{}",
- pn,
- act,
- if stone_owner != player {
- format!(" with a {} stone.", stone_owner)
- } else {
- String::from(".")
- }
- ));
- match game.perform_action(act) {
- Err(e) => {
- gui.message_log.log_error(e);
- if engine_turn {
- gui.message_log.log_error(String::from(
- "Error: external and internal game states disagree.",
- ));
- break;
- }
- }
-
- Ok((pos_vec, win)) => {
- gui.board_gui.update(&game, pos_vec);
- gui.game_log.update(&game);
- if let Some(w) = win {
- gui.message_log.log_message(format!("{}", w));
- break;
- }
- }
- }
- }
- }
- }
- },
+ loop {
+ let inp = gui.query_input(&game, call_proc, p1_white, &engine);
+ if !gui.handle_input(&mut game, call_proc, p1_white, inp) {
+ break;
}
}
- gui.screen.getch();
// board_gui.read_action();
endwin();
}