aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortslil clingman <>2020-10-10 23:00:23 -0400
committertslil clingman <>2020-10-10 23:58:27 -0400
commit2494c42d2fbd2900bdfeebfda39b0b706a45f9fe (patch)
tree2d72ed56575bf0499f7218a94977f9da0057f90c /src
parent6edf7d4cdc19ee80b50c813835b8e36b011232b4 (diff)
Thinking about GUI
Diffstat (limited to 'src')
-rw-r--r--src/consulter.rs6
-rw-r--r--src/gui.rs825
-rw-r--r--src/main.rs41
-rw-r--r--src/tui.rs807
4 files changed, 871 insertions, 808 deletions
diff --git a/src/consulter.rs b/src/consulter.rs
index 78fa07d..379dfea 100644
--- a/src/consulter.rs
+++ b/src/consulter.rs
@@ -20,11 +20,11 @@ use std::io::{Error, ErrorKind, Result, Write};
use std::process::Command;
use crate::game::*;
-use crate::gui::*;
+use crate::tui::*;
pub const FILE_NAME: &str = "takwrap_ptn_consult.ptn";
-pub fn consult_next_action(game: &Game, proc: &str) -> Result<GUIResult> {
+pub fn consult_next_action(game: &Game, proc: &str) -> Result<TUIResult> {
let file = File::create(FILE_NAME)?;
write!(&file, "{}", game)?;
@@ -33,7 +33,7 @@ pub fn consult_next_action(game: &Game, proc: &str) -> Result<GUIResult> {
match String::from_utf8(output.stdout) {
Err(e) => Err(Error::new(ErrorKind::InvalidData, e)),
- Ok(raw) => Ok(GUIResult::Raw(raw.trim().to_string())),
+ Ok(raw) => Ok(TUIResult::Raw(raw.trim().to_string())),
}
}
diff --git a/src/gui.rs b/src/gui.rs
index c633dac..32929ae 100644
--- a/src/gui.rs
+++ b/src/gui.rs
@@ -1,807 +1,50 @@
-/*
- This file is part of Takwrap.
+use fltk::{app, button::*, enums::*, frame::*, text::*, window::*};
- Takwrap is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Foobar is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Foobar. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-extern crate pancurses;
-
-pub use pancurses::Input::*;
-pub use pancurses::*;
-
-use std::fs::File;
-use std::io::Write;
-
-use crate::consulter::*;
-use crate::game::*;
-use crate::parser::*;
-
-#[derive(PartialEq)]
-pub enum GUIResult {
- Undo,
- Quit,
- Save,
- SwitchElement,
- Raw(String),
-}
-
-pub struct GameLog {
- action_window: Window,
- pieces_window: Window,
- has_colours: bool,
- white_colour: u8,
- black_colour: u8,
- num_lines: usize,
- offset: usize,
- action_lines: Vec<String>,
-}
-
-impl GameLog {
- pub fn new(
- ypos: i32,
- xpos: i32,
- height: i32,
- width: i32,
- has_colours: bool,
- white_colour: u8,
- black_colour: u8,
- ) -> GameLog {
- let gl = GameLog {
- pieces_window: newwin(4, width, ypos, xpos),
- action_window: newwin(height - 5, width, ypos + 5, xpos),
- has_colours,
- white_colour,
- black_colour,
- num_lines: (height - 7) as usize,
- offset: 0,
- action_lines: Vec::new(),
- };
-
- gl.action_window.keypad(true);
- gl.action_window.nodelay(false);
- gl
- }
-
- pub fn update(&mut self, game: &Game) {
- self.pieces_window.clear();
-
- if self.has_colours {
- self.pieces_window.attrset(ColorPair(self.white_colour));
- }
- self.pieces_window.addstr(format!(
- "W: {:2}",
- game.get_pieces(Player::White, Stone::Flat)
- ));
-
- self.pieces_window.attrset(Attribute::Normal);
- self.pieces_window.addstr(" ");
-
- if self.has_colours {
- self.pieces_window.attrset(ColorPair(self.black_colour));
- }
- self.pieces_window.addstr(format!(
- "B: {:2}",
- game.get_pieces(Player::Black, Stone::Flat),
- ));
-
- self.pieces_window.attrset(Attribute::Normal);
-
- self.pieces_window
- .addstr(format!(" Variant: {}", game.get_start_type().to_string()));
-
- self.pieces_window.mv(1, 0);
-
- if self.has_colours {
- self.pieces_window.attrset(ColorPair(self.white_colour));
- }
- self.pieces_window.addstr(format!(
- "WC: {}",
- game.get_pieces(Player::White, Stone::Capstone)
- ));
-
- self.pieces_window.attrset(Attribute::Normal);
- self.pieces_window.addstr(" ");
-
- if self.has_colours {
- self.pieces_window.attrset(ColorPair(self.black_colour));
- }
- self.pieces_window.addstr(format!(
- "BC: {}",
- game.get_pieces(Player::Black, Stone::Capstone),
- ));
- self.pieces_window.attrset(Attribute::Normal);
-
- self.pieces_window
- .addstr(format!(" Ply: {}", game.get_ply() + 1));
-
- self.pieces_window.mv(3, 0);
- self.pieces_window.addstr("Active player: ");
- match game.get_current_player() {
- Player::Black => {
- if self.has_colours {
- self.pieces_window.attrset(ColorPair(self.black_colour));
- }
- }
- Player::White => {
- if self.has_colours {
- self.pieces_window.attrset(ColorPair(self.white_colour));
- }
- }
- }
- self.pieces_window.addstr(game.get_current_player_name());
-
- let new_lines = game.get_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();
- }
-
- fn list_actions(&self) {
- self.action_window.clear();
- 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,
- Character('S') => return GUIResult::Save,
- _ => (),
- }
- }
- }
- }
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ Increment,
+ Decrement,
}
-pub struct BoardGUI {
- board_window: Window,
- cell_windows: Vec<Window>,
- size: u8,
- cell_height: u8,
- cell_width: u8,
- hoff: i32,
- cur_cell: Position,
- has_colours: bool,
- white_colour: u8,
- black_colour: u8,
-}
+pub fn gui_test() -> Result<(), Box<dyn std::error::Error>> {
+ let app = app::App::default();
-impl BoardGUI {
- fn draw_grid(&self) {
- let s: i32 = self.size as i32;
- let h: i32 = self.cell_height as i32;
- let w: i32 = self.cell_width as i32;
- for y in 0..h * s + 1 {
- for x in 0..w * s + 1 {
- if y % h == 0 {
- if x % w == 0 {
- self.board_window.mvaddch(y, x + self.hoff, '+');
- } else {
- self.board_window.mvaddch(y, x + self.hoff, '-');
- }
- } else if x % w == 0 {
- self.board_window.mvaddch(y, x + self.hoff, '|');
- }
- }
- }
- for y in 0..s {
- self.board_window
- .mvaddstr(y * h + h / 2, 0, format!("{}.", s - y));
- }
- for x in 0..s {
- self.board_window.mvaddstr(
- s * h + 1,
- w * x + w / 2 + self.hoff,
- format!("{:x}.", x + 10),
- );
- }
- self.board_window.refresh();
- }
+ let mut wind = SingleWindow::default()
+ .with_size(800, 600)
+ .center_screen()
+ .with_label("Test");
+ wind.make_resizable(true);
- fn pos_to_idx(&self, pos: &Position) -> usize {
- let y = pos.y as usize;
- let x = pos.x as usize;
- x + y * (self.size as usize)
- }
+ let mut ptn = TextDisplay::default()
+ .with_size(20 * 16, 30 * 16)
+ .with_pos(800 - 20 * 16 - 10, 0);
+ ptn.set_buffer(Some(TextBuffer::default()));
- fn mv_to_cell(&self, pos: &Position, line: i32) {
- if let Some(win) = self.cell_windows.get(self.pos_to_idx(pos)) {
- win.mv(line, 0);
- win.refresh();
- }
- }
+ let mut but_inc = Button::new(30, 40, 100, 40, "@+");
+ let mut but_dec = Button::new(30, 120, 100, 40, "@square");
+ wind.end();
+ wind.show();
- fn draw_stone(&self, win: &Window, top: bool, buried: bool, player: &Player, stone: &Stone) {
- if self.has_colours && (!buried) {
- match player {
- Player::Black => win.attron(ColorPair(self.black_colour)),
- Player::White => win.attron(ColorPair(self.white_colour)),
- };
- if top {
- win.addch(match stone {
- Stone::Flat => '#',
- Stone::Standing => '/',
- Stone::Capstone => '*',
- });
- } else {
- win.addstr(format!("{}", player));
- }
- win.attrset(Attribute::Normal);
- } else {
- if buried {
- win.attron(Attribute::Underline);
- }
- win.addstr(format!("{}", player));
- if top {
- win.addstr(format!("{}", stone));
- }
- }
- }
+ let (s, r) = app::channel::<Message>();
- fn draw_stack(&self, pos: &Position, stack: &Stack) {
- if let Some(win) = self.cell_windows.get(self.pos_to_idx(pos)) {
- win.clear();
- let height = stack.len() as i32;
- if height > 0 {
- let carry_capacity = self.size;
+ but_inc.emit(s, Message::Increment);
+ but_dec.emit(s, Message::Decrement);
- let my = win.get_max_y();
- let mut cy = 0;
- let mut cx = 0;
+ let mut state = 0;
- for i in 0..height {
- let piece = &stack[(height - i - 1) as usize];
- self.draw_stone(
- win,
- i == 0,
- i >= carry_capacity as i32,
- &piece.player,
- &piece.stone,
- );
- cy += 1;
- if (cy >= my) && (i + 1 < height) {
- cx += 1;
- if i + my - 1 >= height {
- cy = i + my - height + 1;
- } else {
- cy = 1;
- }
- win.mv(0, cx);
- win.attrset(Attribute::Normal);
- win.addch('v');
- }
- win.mv(cy, cx);
- }
+ while app.wait()? {
+ match r.recv() {
+ Some(Message::Increment) => {
+ state += 1;
+ ptn.insert(&format!("Increment {}\n", state));
}
- win.refresh();
- }
- }
- pub fn update(&self, game: &Game, squares: Vec<Position>) {
- // Is this really the `best' way?
- assert!(game.get_size() == self.size);
- for p in squares {
- if let Some(stack) = game.query_square(&p) {
- self.draw_stack(&p, stack);
+ Some(Message::Decrement) => {
+ state -= 1;
+ ptn.insert(&format!("Decrement {}\n", state));
}
+ None => (),
}
- // Prevent redraw of the grid itself?
- self.board_window.untouch();
- }
-
- pub fn new(
- ypos: i32,
- xpos: i32,
- size: u8,
- cell_height: u8,
- cell_width: u8,
- hoff: u8,
- has_colours: bool,
- white_colour: u8,
- black_colour: u8,
- ) -> BoardGUI {
- let hoff = hoff as i32;
- let s: i32 = size as i32;
- let h: i32 = cell_height as i32;
- let w: i32 = cell_width as i32;
-
- let board_window = newwin(h * s + 3, w * s + 3 + hoff, ypos, xpos);
-
- let mut cell_windows: Vec<Window> = Vec::new();
- for i in 0..s * s {
- let y: i32 = i / s;
- let x: i32 = i % s;
- cell_windows.push(newwin(
- h - 1,
- w - 1,
- (s - y - 1) * h + 1 + ypos,
- hoff + x * w + 1 + xpos,
- ));
- }
-
- let bg = BoardGUI {
- board_window,
- cell_windows,
- size,
- cell_height,
- cell_width,
- hoff,
- cur_cell: Position { x: 0, y: 0 },
- has_colours,
- black_colour,
- white_colour,
- };
-
- bg.board_window.keypad(true);
- bg.board_window.nodelay(false);
-
- bg.draw_grid();
-
- bg.mv_to_cell(&bg.cur_cell, 0);
- bg.board_window.refresh();
- bg
- }
-}
-
-pub struct ActionEntry {
- window: Window,
- max_len: usize,
- xstart: i32,
-}
-
-impl ActionEntry {
- pub fn clear_entry_area(&self) {
- self.window.mv(0, self.xstart);
- for _i in 0..self.max_len {
- self.window.addch('_');
- }
-
- self.window.mv(0, self.xstart);
- self.window.refresh();
- }
-
- pub fn new(ypos: i32, xpos: i32, max_len: usize) -> ActionEntry {
- let txt = "Enter action: ";
- let xstart = txt.len() as i32;
- let width = max_len as i32 + xstart + 1;
-
- let ae = ActionEntry {
- window: newwin(1, width, ypos, xpos),
- max_len,
- xstart,
- };
-
- ae.window.nodelay(false);
- ae.window.keypad(true);
-
- ae.window.addstr(txt);
- ae.clear_entry_area();
- ae
- }
-
- pub fn read_action(&self) -> GUIResult {
- self.window.mv(0, self.xstart);
- self.window.refresh();
-
- let mut result = String::new();
- loop {
- if let Some(inp) = self.window.getch() {
- match inp {
- Character('\t') => return GUIResult::SwitchElement,
- Character('\n') => break,
-
- Character(c) => {
- // Annoyingly there are two backspace possibilities
- let ascii = c as usize;
- if !result.is_empty() && (ascii == 127) || (ascii == 8) {
- result.pop();
- let (y, x) = (self.window.get_cur_y(), self.window.get_cur_x());
- self.window.mv(y, x - 1);
- self.window.addch('_');
- self.window.mv(y, x - 1);
- } else {
- if (result.len() < self.max_len) && (ascii > 32) && (ascii < 127) {
- result.push(c);
- self.window.addch(c);
- }
- }
- }
- KeyBackspace => {
- if !result.is_empty() {
- result.pop();
- let (y, x) = (self.window.get_cur_y(), self.window.get_cur_x());
- self.window.mv(y, x - 1);
- self.window.addch('_');
- self.window.mv(y, x - 1);
- }
- }
-
- KeyEnter => break,
- _ => (),
- };
- }
- self.window.refresh();
- }
- self.clear_entry_area();
- return GUIResult::Raw(result);
- }
-}
-
-pub struct MessageLog {
- window: Window,
- has_colours: bool,
- red_colour: u8,
-}
-
-impl MessageLog {
- pub fn new(
- ypos: i32,
- xpos: i32,
- height: i32,
- width: i32,
- has_colours: bool,
- red_colour: u8,
- ) -> MessageLog {
- MessageLog {
- window: newwin(height, width, ypos, xpos),
- has_colours: has_colours,
- red_colour: red_colour,
- }
- }
-
- pub fn log_message(&self, str: String) {
- self.window.clear();
- if self.has_colours {
- self.window.attrset(Attribute::Normal);
- }
- self.window.addstr(str);
- self.window.refresh();
- }
-
- pub fn log_error(&self, str: String) {
- self.window.clear();
- if self.has_colours {
- self.window.attrset(ColorPair(self.red_colour));
- }
- self.window.addstr(str);
- self.window.refresh();
- }
-}
-
-enum InputSource {
- Player,
- Log,
-}
-
-const ACTION_HEADER: &str = "Enter action in PTN";
-const LOG_HEADER: &str = "U to undo a turn | <UP>/<DOWN> to scroll | S to save PTN | Q to quit";
-
-pub struct GUI {
- screen: Window,
- game_log: GameLog,
- pub 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 {} | <TAB> to switch GUI element | {}",
- env!("CARGO_PKG_VERSION").to_string(),
- header
- ));
- self.screen.mvchgat(0, 0, -1, A_REVERSE, -1);
- self.screen.refresh();
- }
-
- fn save(&self, game: &Game) {
- let file = File::create(format!("Game_{}.ptn", game.get_date_string()));
-
- match file {
- Err(e) => {
- self.message_log
- .log_error(format!("Unable to open PTN file for writing: {}", e));
- }
- Ok(file) => {
- if let Err(e) = write!(&file, "{}", game.to_string()) {
- self.message_log
- .log_error(format!("Unable to write PTN to file: {}", e));
- } else {
- self.message_log.log_message(String::from("PTN saved."));
- }
- }
- }
- }
-
- pub fn new(game: &Game, warning: String) -> 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 height = screen.get_max_y() - voff;
-
- screen.refresh();
-
- let cell_height = 4;
- let cell_width = 8;
- let hoff = 3;
- let size = game.get_size();
-
- 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,
- );
-
- let mut gui = GUI {
- screen,
- game_log,
- message_log,
- action_entry,
- board_gui,
- input_source: InputSource::Player,
- };
-
- gui.write_header(ACTION_HEADER);
- gui.message_log.log_error(warning);
- gui.game_log.update(game);
-
- gui
- }
-
- pub 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.get_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()),
- }
- }
-
- pub fn handle_input(
- &mut self,
- game: &mut Game,
- call_proc: bool,
- p1_white: bool,
- input: Result<GUIResult, String>,
- ) -> bool {
- let player = game.get_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."));
- self.screen.getch();
- return false;
- }
- GUIResult::Save => {
- self.save(game);
- self.screen.getch();
- 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.get_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.get_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!(
- "Game over: {}\nPress S to save PTN and quit, or Q to quit.",
- w
- ));
- loop {
- if let Some(inp) = self.screen.getch() {
- match inp {
- Character('S') => {
- self.save(game);
- return false;
- }
- Character('Q') => {
- return false;
- }
- _ => (),
- }
- }
- }
- }
- }
- }
- }
- }
- }
- },
- }
- return true;
}
+ Ok(())
}
diff --git a/src/main.rs b/src/main.rs
index 8ce24d2..c7cc30a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -24,11 +24,13 @@ mod consulter;
mod game;
mod gui;
mod parser;
+mod tui;
use crate::consulter::consulter_clean_up;
use crate::game::*;
use crate::gui::*;
use crate::parser::parse_start_type;
+use crate::tui::*;
use argparse::*;
use chrono::Local;
@@ -44,6 +46,7 @@ fn main() {
let mut engine = String::new();
let mut name = String::new();
let mut size: u8 = 5;
+ let mut terminal = false;
let mut start_type_string = String::new();
println!("Takwrap, a TUI for playing Tak and interacting with Tak engines.\n
@@ -114,6 +117,13 @@ This program comes with ABSOLUTELY NO WARRANTY; and is made available under the
Print(env!("CARGO_PKG_VERSION").to_string()),
"Show version",
);
+
+ ap.refer(&mut terminal).add_option(
+ &["-t", "--terminal"],
+ Store,
+ "Use the terminal user interface instead of the graphical one.",
+ );
+
ap.parse_args_or_exit();
}
@@ -162,23 +172,26 @@ This program comes with ABSOLUTELY NO WARRANTY; and is made available under the
Game::new(size, &p2_name, &p1_name, &date_string, start_type)
};
- let mut gui = GUI::new(&game, warning);
+ if terminal {
+ let mut tui = TUI::new(&game, warning);
- loop {
- let inp = gui.query_input(&game, call_proc, p1_white, &engine);
- if !gui.handle_input(&mut game, call_proc, p1_white, inp) {
- break;
+ loop {
+ let inp = tui.query_input(&game, call_proc, p1_white, &engine);
+ if !tui.handle_input(&mut game, call_proc, p1_white, inp) {
+ break;
+ }
}
- }
- // Clean up file
- if call_proc {
- if let Err(e) = consulter_clean_up() {
- gui.message_log
- .log_error(format!("Error in cleaning up temporary files: {}", e));
+ // Clean up file
+ if call_proc {
+ if let Err(e) = consulter_clean_up() {
+ tui.message_log
+ .log_error(format!("Error in cleaning up temporary files: {}", e));
+ }
}
- }
- // board_gui.read_action();
- endwin();
+ endwin();
+ } else {
+ gui_test();
+ }
}
diff --git a/src/tui.rs b/src/tui.rs
new file mode 100644
index 0000000..037d7ca
--- /dev/null
+++ b/src/tui.rs
@@ -0,0 +1,807 @@
+/*
+ This file is part of Takwrap.
+
+ Takwrap is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Foobar is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Foobar. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+extern crate pancurses;
+
+pub use pancurses::Input::*;
+pub use pancurses::*;
+
+use std::fs::File;
+use std::io::Write;
+
+use crate::consulter::*;
+use crate::game::*;
+use crate::parser::*;
+
+#[derive(PartialEq)]
+pub enum TUIResult {
+ Undo,
+ Quit,
+ Save,
+ SwitchElement,
+ Raw(String),
+}
+
+pub struct GameLog {
+ action_window: Window,
+ pieces_window: Window,
+ has_colours: bool,
+ white_colour: u8,
+ black_colour: u8,
+ num_lines: usize,
+ offset: usize,
+ action_lines: Vec<String>,
+}
+
+impl GameLog {
+ pub fn new(
+ ypos: i32,
+ xpos: i32,
+ height: i32,
+ width: i32,
+ has_colours: bool,
+ white_colour: u8,
+ black_colour: u8,
+ ) -> GameLog {
+ let gl = GameLog {
+ pieces_window: newwin(4, width, ypos, xpos),
+ action_window: newwin(height - 5, width, ypos + 5, xpos),
+ has_colours,
+ white_colour,
+ black_colour,
+ num_lines: (height - 7) as usize,
+ offset: 0,
+ action_lines: Vec::new(),
+ };
+
+ gl.action_window.keypad(true);
+ gl.action_window.nodelay(false);
+ gl
+ }
+
+ pub fn update(&mut self, game: &Game) {
+ self.pieces_window.clear();
+
+ if self.has_colours {
+ self.pieces_window.attrset(ColorPair(self.white_colour));
+ }
+ self.pieces_window.addstr(format!(
+ "W: {:2}",
+ game.get_pieces(Player::White, Stone::Flat)
+ ));
+
+ self.pieces_window.attrset(Attribute::Normal);
+ self.pieces_window.addstr(" ");
+
+ if self.has_colours {
+ self.pieces_window.attrset(ColorPair(self.black_colour));
+ }
+ self.pieces_window.addstr(format!(
+ "B: {:2}",
+ game.get_pieces(Player::Black, Stone::Flat),
+ ));
+
+ self.pieces_window.attrset(Attribute::Normal);
+
+ self.pieces_window
+ .addstr(format!(" Variant: {}", game.get_start_type().to_string()));
+
+ self.pieces_window.mv(1, 0);
+
+ if self.has_colours {
+ self.pieces_window.attrset(ColorPair(self.white_colour));
+ }
+ self.pieces_window.addstr(format!(
+ "WC: {}",
+ game.get_pieces(Player::White, Stone::Capstone)
+ ));
+
+ self.pieces_window.attrset(Attribute::Normal);
+ self.pieces_window.addstr(" ");
+
+ if self.has_colours {
+ self.pieces_window.attrset(ColorPair(self.black_colour));
+ }
+ self.pieces_window.addstr(format!(
+ "BC: {}",
+ game.get_pieces(Player::Black, Stone::Capstone),
+ ));
+ self.pieces_window.attrset(Attribute::Normal);
+
+ self.pieces_window
+ .addstr(format!(" Ply: {}", game.get_ply() + 1));
+
+ self.pieces_window.mv(3, 0);
+ self.pieces_window.addstr("Active player: ");
+ match game.get_current_player() {
+ Player::Black => {
+ if self.has_colours {
+ self.pieces_window.attrset(ColorPair(self.black_colour));
+ }
+ }
+ Player::White => {
+ if self.has_colours {
+ self.pieces_window.attrset(ColorPair(self.white_colour));
+ }
+ }
+ }
+ self.pieces_window.addstr(game.get_current_player_name());
+
+ let new_lines = game.get_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();
+ }
+
+ fn list_actions(&self) {
+ self.action_window.clear();
+ 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) -> TUIResult {
+ 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 TUIResult::SwitchElement,
+ Character('U') => return TUIResult::Undo,
+ Character('Q') => return TUIResult::Quit,
+ Character('S') => return TUIResult::Save,
+ _ => (),
+ }
+ }
+ }
+ }
+}
+
+pub struct BoardTUI {
+ board_window: Window,
+ cell_windows: Vec<Window>,
+ size: u8,
+ cell_height: u8,
+ cell_width: u8,
+ hoff: i32,
+ cur_cell: Position,
+ has_colours: bool,
+ white_colour: u8,
+ black_colour: u8,
+}
+
+impl BoardTUI {
+ fn draw_grid(&self) {
+ let s: i32 = self.size as i32;
+ let h: i32 = self.cell_height as i32;
+ let w: i32 = self.cell_width as i32;
+ for y in 0..h * s + 1 {
+ for x in 0..w * s + 1 {
+ if y % h == 0 {
+ if x % w == 0 {
+ self.board_window.mvaddch(y, x + self.hoff, '+');
+ } else {
+ self.board_window.mvaddch(y, x + self.hoff, '-');
+ }
+ } else if x % w == 0 {
+ self.board_window.mvaddch(y, x + self.hoff, '|');
+ }
+ }
+ }
+ for y in 0..s {
+ self.board_window
+ .mvaddstr(y * h + h / 2, 0, format!("{}.", s - y));
+ }
+ for x in 0..s {
+ self.board_window.mvaddstr(
+ s * h + 1,
+ w * x + w / 2 + self.hoff,
+ format!("{:x}.", x + 10),
+ );
+ }
+ self.board_window.refresh();
+ }
+
+ fn pos_to_idx(&self, pos: &Position) -> usize {
+ let y = pos.y as usize;
+ let x = pos.x as usize;
+ x + y * (self.size as usize)
+ }
+
+ fn mv_to_cell(&self, pos: &Position, line: i32) {
+ if let Some(win) = self.cell_windows.get(self.pos_to_idx(pos)) {
+ win.mv(line, 0);
+ win.refresh();
+ }
+ }
+
+ fn draw_stone(&self, win: &Window, top: bool, buried: bool, player: &Player, stone: &Stone) {
+ if self.has_colours && (!buried) {
+ match player {
+ Player::Black => win.attron(ColorPair(self.black_colour)),
+ Player::White => win.attron(ColorPair(self.white_colour)),
+ };
+ if top {
+ win.addch(match stone {
+ Stone::Flat => '#',
+ Stone::Standing => '/',
+ Stone::Capstone => '*',
+ });
+ } else {
+ win.addstr(format!("{}", player));
+ }
+ win.attrset(Attribute::Normal);
+ } else {
+ if buried {
+ win.attron(Attribute::Underline);
+ }
+ win.addstr(format!("{}", player));
+ if top {
+ win.addstr(format!("{}", stone));
+ }
+ }
+ }
+
+ fn draw_stack(&self, pos: &Position, stack: &Stack) {
+ if let Some(win) = self.cell_windows.get(self.pos_to_idx(pos)) {
+ win.clear();
+ let height = stack.len() as i32;
+ if height > 0 {
+ let carry_capacity = self.size;
+
+ let my = win.get_max_y();
+ let mut cy = 0;
+ let mut cx = 0;
+
+ for i in 0..height {
+ let piece = &stack[(height - i - 1) as usize];
+ self.draw_stone(
+ win,
+ i == 0,
+ i >= carry_capacity as i32,
+ &piece.player,
+ &piece.stone,
+ );
+ cy += 1;
+ if (cy >= my) && (i + 1 < height) {
+ cx += 1;
+ if i + my - 1 >= height {
+ cy = i + my - height + 1;
+ } else {
+ cy = 1;
+ }
+ win.mv(0, cx);
+ win.attrset(Attribute::Normal);
+ win.addch('v');
+ }
+ win.mv(cy, cx);
+ }
+ }
+ win.refresh();
+ }
+ }
+
+ pub fn update(&self, game: &Game, squares: Vec<Position>) {
+ // Is this really the `best' way?
+ assert!(game.get_size() == self.size);
+ for p in squares {
+ if let Some(stack) = game.query_square(&p) {
+ self.draw_stack(&p, stack);
+ }
+ }
+ // Prevent redraw of the grid itself?
+ self.board_window.untouch();
+ }
+
+ pub fn new(
+ ypos: i32,
+ xpos: i32,
+ size: u8,
+ cell_height: u8,
+ cell_width: u8,
+ hoff: u8,
+ has_colours: bool,
+ white_colour: u8,
+ black_colour: u8,
+ ) -> BoardTUI {
+ let hoff = hoff as i32;
+ let s: i32 = size as i32;
+ let h: i32 = cell_height as i32;
+ let w: i32 = cell_width as i32;
+
+ let board_window = newwin(h * s + 3, w * s + 3 + hoff, ypos, xpos);
+
+ let mut cell_windows: Vec<Window> = Vec::new();
+ for i in 0..s * s {
+ let y: i32 = i / s;
+ let x: i32 = i % s;
+ cell_windows.push(newwin(
+ h - 1,
+ w - 1,
+ (s - y - 1) * h + 1 + ypos,
+ hoff + x * w + 1 + xpos,
+ ));
+ }
+
+ let bg = BoardTUI {
+ board_window,
+ cell_windows,
+ size,
+ cell_height,
+ cell_width,
+ hoff,
+ cur_cell: Position { x: 0, y: 0 },
+ has_colours,
+ black_colour,
+ white_colour,
+ };
+
+ bg.board_window.keypad(true);
+ bg.board_window.nodelay(false);
+
+ bg.draw_grid();
+
+ bg.mv_to_cell(&bg.cur_cell, 0);
+ bg.board_window.refresh();
+ bg
+ }
+}
+
+pub struct ActionEntry {
+ window: Window,
+ max_len: usize,
+ xstart: i32,
+}
+
+impl ActionEntry {
+ pub fn clear_entry_area(&self) {
+ self.window.mv(0, self.xstart);
+ for _i in 0..self.max_len {
+ self.window.addch('_');
+ }
+
+ self.window.mv(0, self.xstart);
+ self.window.refresh();
+ }
+
+ pub fn new(ypos: i32, xpos: i32, max_len: usize) -> ActionEntry {
+ let txt = "Enter action: ";
+ let xstart = txt.len() as i32;
+ let width = max_len as i32 + xstart + 1;
+
+ let ae = ActionEntry {
+ window: newwin(1, width, ypos, xpos),
+ max_len,
+ xstart,
+ };
+
+ ae.window.nodelay(false);
+ ae.window.keypad(true);
+
+ ae.window.addstr(txt);
+ ae.clear_entry_area();
+ ae
+ }
+
+ pub fn read_action(&self) -> TUIResult {
+ self.window.mv(0, self.xstart);
+ self.window.refresh();
+
+ let mut result = String::new();
+ loop {
+ if let Some(inp) = self.window.getch() {
+ match inp {
+ Character('\t') => return TUIResult::SwitchElement,
+ Character('\n') => break,
+
+ Character(c) => {
+ // Annoyingly there are two backspace possibilities
+ let ascii = c as usize;
+ if !result.is_empty() && (ascii == 127) || (ascii == 8) {
+ result.pop();
+ let (y, x) = (self.window.get_cur_y(), self.window.get_cur_x());
+ self.window.mv(y, x - 1);
+ self.window.addch('_');
+ self.window.mv(y, x - 1);
+ } else {
+ if (result.len() < self.max_len) && (ascii > 32) && (ascii < 127) {
+ result.push(c);
+ self.window.addch(c);
+ }
+ }
+ }
+ KeyBackspace => {
+ if !result.is_empty() {
+ result.pop();
+ let (y, x) = (self.window.get_cur_y(), self.window.get_cur_x());
+ self.window.mv(y, x - 1);
+ self.window.addch('_');
+ self.window.mv(y, x - 1);
+ }
+ }
+
+ KeyEnter => break,
+ _ => (),
+ };
+ }
+ self.window.refresh();
+ }
+ self.clear_entry_area();
+ return TUIResult::Raw(result);
+ }
+}
+
+pub struct MessageLog {
+ window: Window,
+ has_colours: bool,
+ red_colour: u8,
+}
+
+impl MessageLog {
+ pub fn new(
+ ypos: i32,
+ xpos: i32,
+ height: i32,
+ width: i32,
+ has_colours: bool,
+ red_colour: u8,
+ ) -> MessageLog {
+ MessageLog {
+ window: newwin(height, width, ypos, xpos),
+ has_colours: has_colours,
+ red_colour: red_colour,
+ }
+ }
+
+ pub fn log_message(&self, str: String) {
+ self.window.clear();
+ if self.has_colours {
+ self.window.attrset(Attribute::Normal);
+ }
+ self.window.addstr(str);
+ self.window.refresh();
+ }
+
+ pub fn log_error(&self, str: String) {
+ self.window.clear();
+ if self.has_colours {
+ self.window.attrset(ColorPair(self.red_colour));
+ }
+ self.window.addstr(str);
+ self.window.refresh();
+ }
+}
+
+enum InputSource {
+ Player,
+ Log,
+}
+
+const ACTION_HEADER: &str = "Enter action in PTN";
+const LOG_HEADER: &str = "U to undo a turn | <UP>/<DOWN> to scroll | S to save PTN | Q to quit";
+
+pub struct TUI {
+ screen: Window,
+ game_log: GameLog,
+ pub message_log: MessageLog,
+ action_entry: ActionEntry,
+ board_tui: BoardTUI,
+ input_source: InputSource,
+}
+
+impl TUI {
+ fn write_header(&self, header: &str) {
+ self.screen.mv(0, 0);
+ self.screen.clrtoeol();
+ self.screen.addstr(format!(
+ "takwrap {} | <TAB> to switch TUI element | {}",
+ env!("CARGO_PKG_VERSION").to_string(),
+ header
+ ));
+ self.screen.mvchgat(0, 0, -1, A_REVERSE, -1);
+ self.screen.refresh();
+ }
+
+ fn save(&self, game: &Game) {
+ let file = File::create(format!("Game_{}.ptn", game.get_date_string()));
+
+ match file {
+ Err(e) => {
+ self.message_log
+ .log_error(format!("Unable to open PTN file for writing: {}", e));
+ }
+ Ok(file) => {
+ if let Err(e) = write!(&file, "{}", game.to_string()) {
+ self.message_log
+ .log_error(format!("Unable to write PTN to file: {}", e));
+ } else {
+ self.message_log.log_message(String::from("PTN saved."));
+ }
+ }
+ }
+ }
+
+ pub fn new(game: &Game, warning: String) -> TUI {
+ 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 height = screen.get_max_y() - voff;
+
+ screen.refresh();
+
+ let cell_height = 4;
+ let cell_width = 8;
+ let hoff = 3;
+ let size = game.get_size();
+
+ let board_bottom = voff + (size * cell_height + 3) as i32;
+ let board_right = (hoff + size * cell_width + 3) as i32;
+
+ let board_tui = BoardTUI::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,
+ );
+
+ let mut tui = TUI {
+ screen,
+ game_log,
+ message_log,
+ action_entry,
+ board_tui,
+ input_source: InputSource::Player,
+ };
+
+ tui.write_header(ACTION_HEADER);
+ tui.message_log.log_error(warning);
+ tui.game_log.update(game);
+
+ tui
+ }
+
+ pub fn query_input(
+ &mut self,
+ game: &Game,
+ call_proc: bool,
+ p1_white: bool,
+ engine: &str,
+ ) -> Result<TUIResult, String> {
+ match self.input_source {
+ InputSource::Player => {
+ let player = game.get_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()),
+ }
+ }
+
+ pub fn handle_input(
+ &mut self,
+ game: &mut Game,
+ call_proc: bool,
+ p1_white: bool,
+ input: Result<TUIResult, String>,
+ ) -> bool {
+ let player = game.get_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(tui_result) => match tui_result {
+ TUIResult::Quit => {
+ self.message_log
+ .log_message(String::from("Press any key to quit."));
+ self.screen.getch();
+ return false;
+ }
+ TUIResult::Save => {
+ self.save(game);
+ self.screen.getch();
+ return false;
+ }
+ TUIResult::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);
+ }
+ },
+ TUIResult::Undo => match game.undo() {
+ Ok(pos_vec) => {
+ self.message_log
+ .log_message(String::from("Game undone by one turn."));
+ self.board_tui.update(&game, pos_vec);
+ self.game_log.update(&game);
+ }
+ Err(err) => {
+ self.message_log.log_error(err);
+ }
+ },
+ TUIResult::Raw(raw) => {
+ let stone_owner = game.get_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.get_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_tui.update(&game, pos_vec);
+ self.game_log.update(&game);
+ if let Some(w) = win {
+ self.message_log.log_message(format!(
+ "Game over: {}\nPress S to save PTN and quit, or Q to quit.",
+ w
+ ));
+ loop {
+ if let Some(inp) = self.screen.getch() {
+ match inp {
+ Character('S') => {
+ self.save(game);
+ return false;
+ }
+ Character('Q') => {
+ return false;
+ }
+ _ => (),
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ }
+ return true;
+ }
+}