From a0f3a7f8ba9036d1fcb18941a54077082f15cf3c Mon Sep 17 00:00:00 2001
From: tslil clingman <>
Date: Sat, 9 Jan 2021 22:42:12 -0500
Subject: Added .rustfmt.toml, tabs for indentation, spaces for alignment
---
src/consulter.rs | 38 +-
src/game.rs | 1865 ++++++++++++++++++++++++------------------------
src/gui/boardwidget.rs | 476 ++++++------
src/gui/mod.rs | 204 +++---
src/main.rs | 269 +++----
src/parser.rs | 408 +++++------
src/tui.rs | 1510 ++++++++++++++++++++-------------------
7 files changed, 2389 insertions(+), 2381 deletions(-)
(limited to 'src')
diff --git a/src/consulter.rs b/src/consulter.rs
index c266954..9cdbca6 100644
--- a/src/consulter.rs
+++ b/src/consulter.rs
@@ -1,18 +1,18 @@
/*
- This file is part of Takwrap.
+ 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.
+ 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.
- Takwrap 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.
+ Takwrap 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 Takwrap. If not, see .
+ You should have received a copy of the GNU General Public License
+ along with Takwrap. If not, see .
*/
use std::fs::{remove_file, File};
@@ -25,18 +25,18 @@ use crate::tui::*;
pub const FILE_NAME: &str = "takwrap_ptn_consult.ptn";
pub fn consult_next_action(game: &Game, proc: &str) -> Result {
- let file = File::create(FILE_NAME)?;
+ let file = File::create(FILE_NAME)?;
- write!(&file, "{}", game)?;
+ write!(&file, "{}", game)?;
- let output = Command::new(proc).args(&[FILE_NAME]).output()?;
+ let output = Command::new(proc).args(&[FILE_NAME]).output()?;
- match String::from_utf8(output.stdout) {
- Err(e) => Err(Error::new(ErrorKind::InvalidData, e)),
- Ok(raw) => Ok(TUIResult::Raw(raw.trim().to_string())),
- }
+ match String::from_utf8(output.stdout) {
+ Err(e) => Err(Error::new(ErrorKind::InvalidData, e)),
+ Ok(raw) => Ok(TUIResult::Raw(raw.trim().to_string())),
+ }
}
pub fn consulter_clean_up() -> Result<()> {
- remove_file(FILE_NAME)
+ remove_file(FILE_NAME)
}
diff --git a/src/game.rs b/src/game.rs
index d8fd268..7e96a2e 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -1,18 +1,18 @@
/*
- This file is part of Takwrap.
+ 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.
+ 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.
- Takwrap 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.
+ Takwrap 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 Takwrap. If not, see .
+ You should have received a copy of the GNU General Public License
+ along with Takwrap. If not, see .
*/
use std::collections::HashSet;
@@ -20,136 +20,136 @@ use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Position {
- pub x: u8,
- pub y: u8,
+ pub x: u8,
+ pub y: u8,
}
impl fmt::Display for Position {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{:x}{}", 10 + self.x, self.y + 1)
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:x}{}", 10 + self.x, self.y + 1)
+ }
}
#[derive(PartialEq, Clone, Copy)]
pub enum Player {
- Black,
- White,
+ Black,
+ White,
}
impl fmt::Display for Player {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(
- f,
- "{}",
- match self {
- Player::Black => "B",
- Player::White => "W",
- }
- )
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Player::Black => "B",
+ Player::White => "W",
+ }
+ )
+ }
}
#[derive(PartialEq, Clone, Copy)]
pub enum Stone {
- Flat,
- Standing,
- Capstone,
+ Flat,
+ Standing,
+ Capstone,
}
impl fmt::Display for Stone {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(
- f,
- "{}",
- match self {
- Stone::Flat => "",
- Stone::Standing => "S",
- Stone::Capstone => "C",
- }
- )
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Stone::Flat => "",
+ Stone::Standing => "S",
+ Stone::Capstone => "C",
+ }
+ )
+ }
}
#[derive(Clone, Copy)]
pub enum Direction {
- Up,
- Down,
- Left,
- Right,
+ Up,
+ Down,
+ Left,
+ Right,
}
impl fmt::Display for Direction {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(
- f,
- "{}",
- match self {
- Direction::Up => "+",
- Direction::Down => "-",
- Direction::Left => "<",
- Direction::Right => ">",
- }
- )
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Direction::Up => "+",
+ Direction::Down => "-",
+ Direction::Left => "<",
+ Direction::Right => ">",
+ }
+ )
+ }
}
pub enum Action {
- Place(Player, Position, Stone),
- Move(Position, Direction, u8, Vec),
+ Place(Player, Position, Stone),
+ Move(Position, Direction, u8, Vec),
}
impl fmt::Display for Action {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Action::Place(_, pos, stone) => write!(f, "{}{}", stone, pos),
- Action::Move(pos, direction, picked_up, drops) => {
- match (*picked_up > 1, drops.len() > 1) {
- (true, true) => write!(
- f,
- "{}{}{}{}",
- picked_up,
- pos,
- direction,
- drops.into_iter().map(|q| q.to_string()).collect::(),
- ),
- (true, false) => write!(f, "{}{}{}", picked_up, pos, direction),
- // (false, true) should be impossible
- _ => write!(f, "{}{}", pos, direction),
- }
- }
- }
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Action::Place(_, pos, stone) => write!(f, "{}{}", stone, pos),
+ Action::Move(pos, direction, picked_up, drops) => {
+ match (*picked_up > 1, drops.len() > 1) {
+ (true, true) => write!(
+ f,
+ "{}{}{}{}",
+ picked_up,
+ pos,
+ direction,
+ drops.into_iter().map(|q| q.to_string()).collect::(),
+ ),
+ (true, false) => write!(f, "{}{}{}", picked_up, pos, direction),
+ // (false, true) should be impossible
+ _ => write!(f, "{}{}", pos, direction),
+ }
+ }
+ }
+ }
}
#[derive(Clone, Copy)]
pub struct Piece {
- pub player: Player,
- pub stone: Stone,
+ pub player: Player,
+ pub stone: Stone,
}
#[derive(Clone, Copy)]
pub enum WinType {
- Dragon,
- RoadWin(Player),
- FlatWin(Player),
- Draw,
+ Dragon,
+ RoadWin(Player),
+ FlatWin(Player),
+ Draw,
}
impl fmt::Display for WinType {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(
- f,
- "{}",
- match self {
- WinType::RoadWin(Player::Black) => "0-R",
- WinType::RoadWin(Player::White) => "R-0",
- WinType::FlatWin(Player::Black) => "0-F",
- WinType::FlatWin(Player::White) => "F-0",
- WinType::Dragon => "R-R",
- WinType::Draw => "1/2-1/2",
- }
- )
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ WinType::RoadWin(Player::Black) => "0-R",
+ WinType::RoadWin(Player::White) => "R-0",
+ WinType::FlatWin(Player::Black) => "0-F",
+ WinType::FlatWin(Player::White) => "F-0",
+ WinType::Dragon => "R-R",
+ WinType::Draw => "1/2-1/2",
+ }
+ )
+ }
}
pub type Stack = Vec;
@@ -158,8 +158,8 @@ pub type Stack = Vec;
const-generics would solve a lot of my consistency problems here,
GameState {
- ...
- board: [Stack; size],
+ ...
+ board: [Stack; size],
}
but at the time of writing this is not yet part of the language.
@@ -168,863 +168,860 @@ dependent type system ;)
*/
struct GameState {
- size: u8,
- black_flats: u8,
- white_flats: u8,
- black_capstones: u8,
- white_capstones: u8,
- board: Vec,
+ size: u8,
+ black_flats: u8,
+ white_flats: u8,
+ black_capstones: u8,
+ white_capstones: u8,
+ board: Vec,
}
// TODO: Generate all legal actions for a given player
impl GameState {
- fn copy(&self) -> GameState {
- let mut copy = GameState {
- size: self.size,
- black_flats: self.black_flats,
- white_flats: self.white_flats,
- black_capstones: self.black_capstones,
- white_capstones: self.white_capstones,
- board: Vec::new(),
- };
- for i in 0..self.board.len() {
- copy.board.push(Vec::new());
- for j in 0..self.board[i].len() {
- copy.board[i].push(self.board[i][j]);
- }
- }
- copy
- }
-
- // Defaults to 5x5 if requested things are out of range
- fn new(size: u8) -> GameState {
- let (flats, caps) = match size {
- 3 => (10, 0),
- 4 => (15, 0),
- 6 => (30, 1),
- 7 => (40, 2),
- 8 => (50, 2),
- _ => (21, 1),
- };
- GameState {
- size: size,
- black_flats: flats,
- white_flats: flats,
- black_capstones: caps,
- white_capstones: caps,
- board: {
- let mut v: Vec = Vec::new();
- for _i in 0..size * size {
- v.push(Vec::new());
- }
- v
- },
- }
- }
-
- fn remaining_pieces(&self, player: Player, stone: Stone) -> u8 {
- match player {
- Player::Black => {
- if stone == Stone::Capstone {
- self.black_capstones
- } else {
- self.black_flats
- }
- }
- Player::White => {
- if stone == Stone::Capstone {
- self.white_capstones
- } else {
- self.white_flats
- }
- }
- }
- }
-
- fn within_bounds(&self, pos: &Position) -> bool {
- if (pos.x < self.size) && (pos.y < self.size) {
- true
- } else {
- false
- }
- }
-
- fn pos_to_idx(&self, pos: &Position) -> usize {
- let y: usize = pos.y as usize;
- let x: usize = pos.x as usize;
- x + y * (self.size as usize)
- }
-
- fn query_pos(&self, pos: &Position) -> Option<&Stack> {
- self.board.get(self.pos_to_idx(pos))
- }
-
- fn is_legal_place(
- &self,
- player: Player,
- pos: &Position,
- stone: Stone,
- check_start: Option,
- ) -> Result<(), String> {
- /* In order to legally place a piece:
- 1. The desired square must be empty
- 2. The player must have sufficient pieces
- 3. We must account for F{C,E}S
- */
- if self.within_bounds(pos) {
- if let Some(start_type) = check_start {
- match start_type {
- StartType::FCS => {
- if ((pos.x == 0) && (pos.y == 0))
- || ((pos.x + 1 == self.size) && (pos.y == 0))
- || ((pos.x == 0) && (pos.y + 1 == self.size))
- || ((pos.x + 1 == self.size) && (pos.y + 1 == self.size))
- {
- return Err(format!(
- "At the start of game in FCS, {} may not be placed in a corner (such as {}).",
- player, pos
- ));
- }
- }
- StartType::FES => {
- if (pos.x == 0)
- || (pos.y == 0)
- || (pos.x + 1 == self.size)
- || (pos.y + 1 == self.size)
- {
- return Err(format!(
- "At the start of a game in FES, {} may not be placed along any edge (and {} is on an edge).",
- player, pos
- ));
- }
- }
- _ => (),
- }
- }
-
- if let Some(stack) = &self.board.get(self.pos_to_idx(pos)) {
- if stack.len() > 0 {
- return Err(format!("{} is already occupied.", pos));
- } else if self.remaining_pieces(player, stone) == 0 {
- return Err(format!(
- "{} has no more remaining {} pieces.",
- player, stone
- ));
- } else {
- return Ok(());
- }
- } else {
- return Err(format!(
- "Internal error: lookup for {} failed in is_legal_place.",
- pos
- ));
- }
- } else {
- return Err(format!("Position {} is not within bounds.", pos));
- }
- }
-
- fn place_stone(
- &self,
- player: Player,
- pos: &Position,
- stone: Stone,
- check_start: Option,
- ) -> Result<(Vec, GameState), String> {
- self.is_legal_place(player, pos, stone, check_start)?;
-
- // Place stone
- let mut copy = self.copy();
- let idx = self.pos_to_idx(&pos);
- copy.board[idx].push(Piece {
- player: player,
- stone: stone,
- });
-
- // Decrease count
- match player {
- Player::Black => {
- if stone == Stone::Capstone {
- copy.black_capstones -= 1;
- } else {
- copy.black_flats -= 1;
- }
- }
- Player::White => {
- if stone == Stone::Capstone {
- copy.white_capstones -= 1;
- } else {
- copy.white_flats -= 1;
- }
- }
- }
- Ok((vec![pos.clone()], copy))
- }
-
- fn is_legal_move(
- &self,
- player: Player,
- pos: &Position,
- direction: Direction,
- picked_up: u8,
- drops: &Vec,
- ) -> Result<(), String> {
- if self.within_bounds(pos) {
- /* Rules for moving a stack:
- - There are stones
- - Drops have been specified
- - Must actually move at least one stone
- - Top stone belongs to player
- - One or more on each subsequent square
- - Total number of stones picked up does not exceed the carry capacity
- - Direction does not contain a capstone
- - Wall may only appear on last spot if it's capstone alone that covers
- - All stones are used up before then end of the board is met
- */
- if let Some(stack) = &self.board.get(self.pos_to_idx(pos)) {
- let stack_len = stack.len();
- let drops_len = drops.len();
-
- if stack_len == 0 {
- return Err(format!("{} has no stones to move.", pos));
- };
-
- if drops_len == 0 {
- return Err(String::from(
- "A drop sequence for must be specified for a move.",
- ));
- };
-
- if picked_up == 0 {
- return Err(String::from(
- "A valid move must change the position of at least a single stone.",
- ));
- }
-
- if stack[stack_len - 1].player != player {
- return Err(format!(
- "{} may not move the stack at {} as it belongs to {}.",
- player,
- pos,
- stack[stack_len - 1].player
- ));
- };
-
- for i in 0..drops_len {
- if drops[i] == 0 {
- return Err(String::from(
- "A move may not drop 0 stones on subsequent squares.",
- ));
- }
- }
-
- let picked_up = picked_up as usize;
- let sum_dropped = drops.iter().map(|&d| d as usize).sum::();
- if sum_dropped != picked_up {
- return Err(format!(
- "Move intended to pick up {} stones but drop {}.",
- picked_up, sum_dropped
- ));
- }
-
- let carry_capacity = self.size as usize;
- if picked_up > carry_capacity {
- return Err(format!(
- "A move may not pick up more than the carry capacity of {} stones.",
- self.size
- ));
- }
-
- let mut steps: usize = drops.len();
- let cap: bool = stack[0].stone == Stone::Capstone;
- let mut pos_new = Position { x: pos.x, y: pos.y };
-
- while steps > 0 {
- if {
- match direction {
- Direction::Up => pos_new.y + 1 >= self.size,
- Direction::Down => pos_new.y == 0,
- Direction::Left => pos_new.x == 0,
- Direction::Right => pos_new.x + 1 >= self.size,
- }
- } {
- return Err(String::from("A move may not extend past the board."));
- } else {
- match direction {
- Direction::Up => pos_new.y += 1,
- Direction::Down => pos_new.y -= 1,
- Direction::Left => pos_new.x -= 1,
- Direction::Right => pos_new.x += 1,
- }
- if let Some(stack) = &self.board.get(self.pos_to_idx(&pos_new)) {
- if stack.len() > 0 {
- match stack[stack.len() - 1].stone {
- Stone::Capstone => {
- return Err(String::from(
- "A move may not cover a capstone.",
- ));
- }
- Stone::Standing => {
- if (steps > 1) || (!cap) {
- return Err(String::from(
- "A move may not cover a standing stone.",
- ));
- }
- }
- Stone::Flat => (),
- }
- }
- } else {
- return Err(format!(
- "Internal error: lookup for {} failed in is_legal_move (1).",
- pos_new
- ));
- }
- }
- steps -= 1;
- }
- return Ok(());
- } else {
- return Err(format!(
- "Internal error: lookup for {} failed in is_legal_move (2).",
- pos
- ));
- }
- } else {
- return Err(format!("Position {} is not within bounds.", pos));
- }
- }
-
- fn move_stack(
- &self,
- player: Player,
- pos: &Position,
- direction: Direction,
- picked_up: u8,
- drops: &Vec,
- ) -> Result<(Vec, GameState), String> {
- self.is_legal_move(player, pos, direction, picked_up, drops)?;
-
- let mut copy = self.copy();
- let mut pos_vec: Vec = vec![pos.clone()];
-
- let idx = copy.pos_to_idx(&pos);
- let stack: &Stack = &self.board[self.pos_to_idx(pos)];
-
- let stack_height = copy.board[idx].len();
-
- let mut offset = stack_height - (picked_up as usize);
-
- for _i in offset..stack_height {
- copy.board[idx].pop();
- }
-
- for drop_idx in 0..drops.len() {
- let new_pos = pos_vec[pos_vec.len() - 1];
- pos_vec.push(match direction {
- Direction::Up => Position {
- x: new_pos.x,
- y: new_pos.y + 1,
- },
- Direction::Down => Position {
- x: new_pos.x,
- y: new_pos.y - 1,
- },
- Direction::Left => Position {
- x: new_pos.x - 1,
- y: new_pos.y,
- },
- Direction::Right => Position {
- x: new_pos.x + 1,
- y: new_pos.y,
- },
- });
-
- let new_pos = pos_vec[pos_vec.len() - 1];
- let idx = copy.pos_to_idx(&new_pos);
- let num = drops[drop_idx] as usize;
-
- for i in 0..num {
- // Are we flattening?
- if stack[offset + i].stone == Stone::Capstone {
- if let Some(top_piece) = copy.board[idx].last() {
- if top_piece.stone == Stone::Standing {
- let top_stone_idx = copy.board[idx].len() - 1;
- copy.board[idx][top_stone_idx].stone = Stone::Flat;
- }
- }
- }
- copy.board[idx].push(stack[offset + i]);
- }
- offset += num;
- }
- Ok((pos_vec, copy))
- }
-
- // Either up or right
- fn dfs(&self, player: Player, up: bool, mut todo: Vec) -> bool {
- let mut seen: HashSet = HashSet::new();
- for i in 0..todo.len() {
- seen.insert(todo[i].clone());
- }
- loop {
- let more = todo.pop();
- if let Some(p) = more {
- if p.y + 1 < self.size {
- let p_up = Position { x: p.x, y: p.y + 1 };
- let i_up = self.pos_to_idx(&p_up);
- if !seen.contains(&p_up) && self.within_bounds(&p_up) {
- let l = self.board[i_up].len();
- if l > 0 {
- let piece = self.board[i_up][l - 1];
- if (piece.player == player) && (piece.stone != Stone::Standing) {
- if up && (p_up.y + 1 == self.size) {
- return true;
- } else {
- seen.insert(p_up);
- todo.push(p_up);
- }
- }
- }
- }
- }
-
- if p.x + 1 < self.size {
- let p_right = Position { x: p.x + 1, y: p.y };
- let i_right = self.pos_to_idx(&p_right);
- if !seen.contains(&p_right) && self.within_bounds(&p_right) {
- let l = self.board[i_right].len();
- if l > 0 {
- let piece = self.board[i_right][l - 1];
- if (piece.player == player) && (piece.stone != Stone::Standing) {
- if !up && (p_right.x + 1 == self.size) {
- return true;
- } else {
- seen.insert(p_right);
- todo.push(p_right);
- }
- }
- }
- }
- }
-
- if p.y > 0 {
- let p_down = Position { x: p.x, y: p.y - 1 };
- let i_down = self.pos_to_idx(&p_down);
- if !seen.contains(&p_down) && self.within_bounds(&p_down) {
- let l = self.board[i_down].len();
- if l > 0 {
- let piece = self.board[i_down][l - 1];
- if (piece.player == player) && (piece.stone != Stone::Standing) {
- seen.insert(p_down);
- todo.push(p_down);
- }
- }
- }
- }
-
- if p.x > 0 {
- let p_left = Position { x: p.x - 1, y: p.y };
- let i_left = self.pos_to_idx(&p_left);
- if !seen.contains(&p_left) && self.within_bounds(&p_left) {
- let l = self.board[i_left].len();
- if l > 0 {
- let piece = self.board[i_left][l - 1];
- if (piece.player == player) && (piece.stone != Stone::Standing) {
- seen.insert(p_left);
- todo.push(p_left);
- }
- }
- }
- }
-
- seen.insert(p);
- } else {
- break;
- }
- }
- return false;
- }
-
- fn check_road_win(&self) -> Option {
- // Depth first search from bottom edge and left edge
-
- let mut todo_black_up: Vec = Vec::new();
- let mut todo_white_up: Vec = Vec::new();
- let mut todo_black_right: Vec = Vec::new();
- let mut todo_white_right: Vec = Vec::new();
-
- for i in 0..self.size {
- let pos_up = Position { x: i, y: 0 };
- let pos_right = Position { x: 0, y: i };
- let stack_up = &self.board[self.pos_to_idx(&pos_up)];
- let stack_right = &self.board[self.pos_to_idx(&pos_right)];
-
- if !stack_up.is_empty() {
- match stack_up[stack_up.len() - 1].player {
- Player::Black => todo_black_up.push(pos_up),
- Player::White => todo_white_up.push(pos_up),
- }
- }
-
- if !stack_right.is_empty() {
- match stack_right[stack_right.len() - 1].player {
- Player::Black => todo_black_right.push(pos_right),
- Player::White => todo_white_right.push(pos_right),
- }
- }
- }
-
- let black_road = self.dfs(Player::Black, true, todo_black_up)
- || self.dfs(Player::Black, false, todo_black_right);
- let white_road = self.dfs(Player::White, true, todo_white_up)
- || self.dfs(Player::White, false, todo_white_right);
-
- match (black_road, white_road) {
- (false, false) => return None,
- (true, false) => return Some(WinType::RoadWin(Player::Black)),
- (false, true) => return Some(WinType::RoadWin(Player::White)),
- (true, true) => return Some(WinType::Dragon),
- }
- }
-
- fn check_flat_win(&self) -> Option {
- let full_board = self.board.iter().all(|s| s.len() > 0);
- let finished_flats = (self.black_flats == 0) || (self.white_flats == 0);
-
- if full_board || finished_flats {
- let count = self.board.iter().fold(0, |c, s| match s.last() {
- None => c,
- Some(piece) => {
- if piece.stone == Stone::Flat {
- match piece.player {
- Player::Black => c + 1,
- Player::White => c - 1,
- }
- } else {
- c
- }
- }
- });
- // Trichotomy of reals ...
- if count > 0 {
- return Some(WinType::FlatWin(Player::Black));
- } else if count == 0 {
- return Some(WinType::Draw);
- } else {
- return Some(WinType::FlatWin(Player::White));
- }
- }
- return None;
- }
-
- fn check_win(&self) -> Option {
- if let Some(w) = self.check_road_win() {
- return Some(w);
- } else {
- return self.check_flat_win();
- }
- }
+ fn copy(&self) -> GameState {
+ let mut copy = GameState {
+ size: self.size,
+ black_flats: self.black_flats,
+ white_flats: self.white_flats,
+ black_capstones: self.black_capstones,
+ white_capstones: self.white_capstones,
+ board: Vec::new(),
+ };
+ for i in 0..self.board.len() {
+ copy.board.push(Vec::new());
+ for j in 0..self.board[i].len() {
+ copy.board[i].push(self.board[i][j]);
+ }
+ }
+ copy
+ }
+
+ // Defaults to 5x5 if requested things are out of range
+ fn new(size: u8) -> GameState {
+ let (flats, caps) = match size {
+ 3 => (10, 0),
+ 4 => (15, 0),
+ 6 => (30, 1),
+ 7 => (40, 2),
+ 8 => (50, 2),
+ _ => (21, 1),
+ };
+ GameState {
+ size: size,
+ black_flats: flats,
+ white_flats: flats,
+ black_capstones: caps,
+ white_capstones: caps,
+ board: {
+ let mut v: Vec = Vec::new();
+ for _i in 0..size * size {
+ v.push(Vec::new());
+ }
+ v
+ },
+ }
+ }
+
+ fn remaining_pieces(&self, player: Player, stone: Stone) -> u8 {
+ match player {
+ Player::Black => {
+ if stone == Stone::Capstone {
+ self.black_capstones
+ } else {
+ self.black_flats
+ }
+ }
+ Player::White => {
+ if stone == Stone::Capstone {
+ self.white_capstones
+ } else {
+ self.white_flats
+ }
+ }
+ }
+ }
+
+ fn within_bounds(&self, pos: &Position) -> bool {
+ if (pos.x < self.size) && (pos.y < self.size) {
+ true
+ } else {
+ false
+ }
+ }
+
+ fn pos_to_idx(&self, pos: &Position) -> usize {
+ let y: usize = pos.y as usize;
+ let x: usize = pos.x as usize;
+ x + y * (self.size as usize)
+ }
+
+ fn query_pos(&self, pos: &Position) -> Option<&Stack> {
+ self.board.get(self.pos_to_idx(pos))
+ }
+
+ fn is_legal_place(
+ &self,
+ player: Player,
+ pos: &Position,
+ stone: Stone,
+ check_start: Option,
+ ) -> Result<(), String> {
+ /* In order to legally place a piece:
+ 1. The desired square must be empty
+ 2. The player must have sufficient pieces
+ 3. We must account for F{C,E}S
+ */
+ if self.within_bounds(pos) {
+ if let Some(start_type) = check_start {
+ match start_type {
+ StartType::FCS => {
+ if ((pos.x == 0) && (pos.y == 0))
+ || ((pos.x + 1 == self.size) && (pos.y == 0))
+ || ((pos.x == 0) && (pos.y + 1 == self.size))
+ || ((pos.x + 1 == self.size) && (pos.y + 1 == self.size))
+ {
+ return Err(format!(
+ "At the start of game in FCS, {} may not be placed in a corner (such as {}).",
+ player, pos
+ ));
+ }
+ }
+ StartType::FES => {
+ if (pos.x == 0) || (pos.y == 0) || (pos.x + 1 == self.size) || (pos.y + 1 == self.size)
+ {
+ return Err(format!(
+ "At the start of a game in FES, {} may not be placed along any edge (and {} is on an edge).",
+ player, pos
+ ));
+ }
+ }
+ _ => (),
+ }
+ }
+
+ if let Some(stack) = &self.board.get(self.pos_to_idx(pos)) {
+ if stack.len() > 0 {
+ return Err(format!("{} is already occupied.", pos));
+ } else if self.remaining_pieces(player, stone) == 0 {
+ return Err(format!(
+ "{} has no more remaining {} pieces.",
+ player, stone
+ ));
+ } else {
+ return Ok(());
+ }
+ } else {
+ return Err(format!(
+ "Internal error: lookup for {} failed in is_legal_place.",
+ pos
+ ));
+ }
+ } else {
+ return Err(format!("Position {} is not within bounds.", pos));
+ }
+ }
+
+ fn place_stone(
+ &self,
+ player: Player,
+ pos: &Position,
+ stone: Stone,
+ check_start: Option,
+ ) -> Result<(Vec, GameState), String> {
+ self.is_legal_place(player, pos, stone, check_start)?;
+
+ // Place stone
+ let mut copy = self.copy();
+ let idx = self.pos_to_idx(&pos);
+ copy.board[idx].push(Piece {
+ player: player,
+ stone: stone,
+ });
+
+ // Decrease count
+ match player {
+ Player::Black => {
+ if stone == Stone::Capstone {
+ copy.black_capstones -= 1;
+ } else {
+ copy.black_flats -= 1;
+ }
+ }
+ Player::White => {
+ if stone == Stone::Capstone {
+ copy.white_capstones -= 1;
+ } else {
+ copy.white_flats -= 1;
+ }
+ }
+ }
+ Ok((vec![pos.clone()], copy))
+ }
+
+ fn is_legal_move(
+ &self,
+ player: Player,
+ pos: &Position,
+ direction: Direction,
+ picked_up: u8,
+ drops: &Vec,
+ ) -> Result<(), String> {
+ if self.within_bounds(pos) {
+ /* Rules for moving a stack:
+ - There are stones
+ - Drops have been specified
+ - Must actually move at least one stone
+ - Top stone belongs to player
+ - One or more on each subsequent square
+ - Total number of stones picked up does not exceed the carry capacity
+ - Direction does not contain a capstone
+ - Wall may only appear on last spot if it's capstone alone that covers
+ - All stones are used up before then end of the board is met
+ */
+ if let Some(stack) = &self.board.get(self.pos_to_idx(pos)) {
+ let stack_len = stack.len();
+ let drops_len = drops.len();
+
+ if drops_len == 0 {
+ return Err(String::from(
+ "A drop sequence for must be specified for a move.",
+ ));
+ };
+
+ if picked_up == 0 {
+ return Err(String::from(
+ "A valid move must change the position of at least a single stone.",
+ ));
+ }
+
+ if stack[stack_len - 1].player != player {
+ return Err(format!(
+ "{} may not move the stack at {} as it belongs to {}.",
+ player,
+ pos,
+ stack[stack_len - 1].player
+ ));
+ };
+
+ for i in 0..drops_len {
+ if drops[i] == 0 {
+ return Err(String::from(
+ "A move may not drop 0 stones on subsequent squares.",
+ ));
+ }
+ }
+
+ let picked_up = picked_up as usize;
+ let sum_dropped = drops.iter().map(|&d| d as usize).sum::();
+ if sum_dropped != picked_up {
+ return Err(format!(
+ "Move intended to pick up {} stones but drop {}.",
+ picked_up, sum_dropped
+ ));
+ }
+
+ let carry_capacity = self.size as usize;
+ if picked_up > carry_capacity {
+ return Err(format!(
+ "A move may not pick up more than the carry capacity of {} stones.",
+ self.size
+ ));
+ }
+
+ if stack_len < picked_up {
+ return Err(format!("{} has insuficiently many stones.", pos));
+ };
+
+ let mut steps: usize = drops.len();
+ let cap: bool = stack[0].stone == Stone::Capstone;
+ let mut pos_new = Position { x: pos.x, y: pos.y };
+
+ while steps > 0 {
+ if {
+ match direction {
+ Direction::Up => pos_new.y + 1 >= self.size,
+ Direction::Down => pos_new.y == 0,
+ Direction::Left => pos_new.x == 0,
+ Direction::Right => pos_new.x + 1 >= self.size,
+ }
+ } {
+ return Err(String::from("A move may not extend past the board."));
+ } else {
+ match direction {
+ Direction::Up => pos_new.y += 1,
+ Direction::Down => pos_new.y -= 1,
+ Direction::Left => pos_new.x -= 1,
+ Direction::Right => pos_new.x += 1,
+ }
+ if let Some(stack) = &self.board.get(self.pos_to_idx(&pos_new)) {
+ if stack.len() > 0 {
+ match stack[stack.len() - 1].stone {
+ Stone::Capstone => {
+ return Err(String::from("A move may not cover a capstone."));
+ }
+ Stone::Standing => {
+ if (steps > 1) || (!cap) {
+ return Err(String::from("A move may not cover a standing stone."));
+ }
+ }
+ Stone::Flat => (),
+ }
+ }
+ } else {
+ return Err(format!(
+ "Internal error: lookup for {} failed in is_legal_move (1).",
+ pos_new
+ ));
+ }
+ }
+ steps -= 1;
+ }
+ return Ok(());
+ } else {
+ return Err(format!(
+ "Internal error: lookup for {} failed in is_legal_move (2).",
+ pos
+ ));
+ }
+ } else {
+ return Err(format!("Position {} is not within bounds.", pos));
+ }
+ }
+
+ fn move_stack(
+ &self,
+ player: Player,
+ pos: &Position,
+ direction: Direction,
+ picked_up: u8,
+ drops: &Vec,
+ ) -> Result<(Vec, GameState), String> {
+ self.is_legal_move(player, pos, direction, picked_up, drops)?;
+
+ let mut copy = self.copy();
+ let mut pos_vec: Vec = vec![pos.clone()];
+
+ let idx = copy.pos_to_idx(&pos);
+ let stack: &Stack = &self.board[self.pos_to_idx(pos)];
+
+ let stack_height = copy.board[idx].len();
+
+ let mut offset = stack_height - (picked_up as usize);
+
+ for _i in offset..stack_height {
+ copy.board[idx].pop();
+ }
+
+ for drop_idx in 0..drops.len() {
+ let new_pos = pos_vec[pos_vec.len() - 1];
+ pos_vec.push(match direction {
+ Direction::Up => Position {
+ x: new_pos.x,
+ y: new_pos.y + 1,
+ },
+ Direction::Down => Position {
+ x: new_pos.x,
+ y: new_pos.y - 1,
+ },
+ Direction::Left => Position {
+ x: new_pos.x - 1,
+ y: new_pos.y,
+ },
+ Direction::Right => Position {
+ x: new_pos.x + 1,
+ y: new_pos.y,
+ },
+ });
+
+ let new_pos = pos_vec[pos_vec.len() - 1];
+ let idx = copy.pos_to_idx(&new_pos);
+ let num = drops[drop_idx] as usize;
+
+ for i in 0..num {
+ // Are we flattening?
+ if stack[offset + i].stone == Stone::Capstone {
+ if let Some(top_piece) = copy.board[idx].last() {
+ if top_piece.stone == Stone::Standing {
+ let top_stone_idx = copy.board[idx].len() - 1;
+ copy.board[idx][top_stone_idx].stone = Stone::Flat;
+ }
+ }
+ }
+ copy.board[idx].push(stack[offset + i]);
+ }
+ offset += num;
+ }
+ Ok((pos_vec, copy))
+ }
+
+ // Either up or right
+ fn dfs(&self, player: Player, up: bool, mut todo: Vec) -> bool {
+ let mut seen: HashSet = HashSet::new();
+ for i in 0..todo.len() {
+ seen.insert(todo[i].clone());
+ }
+ loop {
+ let more = todo.pop();
+ if let Some(p) = more {
+ if p.y + 1 < self.size {
+ let p_up = Position { x: p.x, y: p.y + 1 };
+ let i_up = self.pos_to_idx(&p_up);
+ if !seen.contains(&p_up) && self.within_bounds(&p_up) {
+ let l = self.board[i_up].len();
+ if l > 0 {
+ let piece = self.board[i_up][l - 1];
+ if (piece.player == player) && (piece.stone != Stone::Standing) {
+ if up && (p_up.y + 1 == self.size) {
+ return true;
+ } else {
+ seen.insert(p_up);
+ todo.push(p_up);
+ }
+ }
+ }
+ }
+ }
+
+ if p.x + 1 < self.size {
+ let p_right = Position { x: p.x + 1, y: p.y };
+ let i_right = self.pos_to_idx(&p_right);
+ if !seen.contains(&p_right) && self.within_bounds(&p_right) {
+ let l = self.board[i_right].len();
+ if l > 0 {
+ let piece = self.board[i_right][l - 1];
+ if (piece.player == player) && (piece.stone != Stone::Standing) {
+ if !up && (p_right.x + 1 == self.size) {
+ return true;
+ } else {
+ seen.insert(p_right);
+ todo.push(p_right);
+ }
+ }
+ }
+ }
+ }
+
+ if p.y > 0 {
+ let p_down = Position { x: p.x, y: p.y - 1 };
+ let i_down = self.pos_to_idx(&p_down);
+ if !seen.contains(&p_down) && self.within_bounds(&p_down) {
+ let l = self.board[i_down].len();
+ if l > 0 {
+ let piece = self.board[i_down][l - 1];
+ if (piece.player == player) && (piece.stone != Stone::Standing) {
+ seen.insert(p_down);
+ todo.push(p_down);
+ }
+ }
+ }
+ }
+
+ if p.x > 0 {
+ let p_left = Position { x: p.x - 1, y: p.y };
+ let i_left = self.pos_to_idx(&p_left);
+ if !seen.contains(&p_left) && self.within_bounds(&p_left) {
+ let l = self.board[i_left].len();
+ if l > 0 {
+ let piece = self.board[i_left][l - 1];
+ if (piece.player == player) && (piece.stone != Stone::Standing) {
+ seen.insert(p_left);
+ todo.push(p_left);
+ }
+ }
+ }
+ }
+
+ seen.insert(p);
+ } else {
+ break;
+ }
+ }
+ return false;
+ }
+
+ fn check_road_win(&self) -> Option {
+ // Depth first search from bottom edge and left edge
+
+ let mut todo_black_up: Vec = Vec::new();
+ let mut todo_white_up: Vec = Vec::new();
+ let mut todo_black_right: Vec = Vec::new();
+ let mut todo_white_right: Vec = Vec::new();
+
+ for i in 0..self.size {
+ let pos_up = Position { x: i, y: 0 };
+ let pos_right = Position { x: 0, y: i };
+ let stack_up = &self.board[self.pos_to_idx(&pos_up)];
+ let stack_right = &self.board[self.pos_to_idx(&pos_right)];
+
+ if !stack_up.is_empty() {
+ match stack_up[stack_up.len() - 1].player {
+ Player::Black => todo_black_up.push(pos_up),
+ Player::White => todo_white_up.push(pos_up),
+ }
+ }
+
+ if !stack_right.is_empty() {
+ match stack_right[stack_right.len() - 1].player {
+ Player::Black => todo_black_right.push(pos_right),
+ Player::White => todo_white_right.push(pos_right),
+ }
+ }
+ }
+
+ let black_road = self.dfs(Player::Black, true, todo_black_up)
+ || self.dfs(Player::Black, false, todo_black_right);
+ let white_road = self.dfs(Player::White, true, todo_white_up)
+ || self.dfs(Player::White, false, todo_white_right);
+
+ match (black_road, white_road) {
+ (false, false) => return None,
+ (true, false) => return Some(WinType::RoadWin(Player::Black)),
+ (false, true) => return Some(WinType::RoadWin(Player::White)),
+ (true, true) => return Some(WinType::Dragon),
+ }
+ }
+
+ fn check_flat_win(&self) -> Option {
+ let full_board = self.board.iter().all(|s| s.len() > 0);
+ let finished_flats = (self.black_flats == 0) || (self.white_flats == 0);
+
+ if full_board || finished_flats {
+ let count = self.board.iter().fold(0, |c, s| match s.last() {
+ None => c,
+ Some(piece) => {
+ if piece.stone == Stone::Flat {
+ match piece.player {
+ Player::Black => c + 1,
+ Player::White => c - 1,
+ }
+ } else {
+ c
+ }
+ }
+ });
+ // Trichotomy of reals ...
+ if count > 0 {
+ return Some(WinType::FlatWin(Player::Black));
+ } else if count == 0 {
+ return Some(WinType::Draw);
+ } else {
+ return Some(WinType::FlatWin(Player::White));
+ }
+ }
+ return None;
+ }
+
+ fn check_win(&self) -> Option {
+ if let Some(w) = self.check_road_win() {
+ return Some(w);
+ } else {
+ return self.check_flat_win();
+ }
+ }
}
enum TurnOrder {
- WhitePlacesBlack,
- BlackPlacesWhite,
- Normal,
+ WhitePlacesBlack,
+ BlackPlacesWhite,
+ Normal,
}
#[derive(Clone, Copy)]
pub enum StartType {
- CPS(u8),
- FCS,
- FES,
- TPS,
- CZS,
+ CPS(u8),
+ FCS,
+ FES,
+ TPS,
+ CZS,
}
impl fmt::Display for StartType {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- StartType::CPS(n) => write!(f, "CPS{}", n),
- StartType::FCS => write!(f, "FCS"),
- StartType::FES => write!(f, "FES"),
- StartType::TPS => write!(f, "TPS"),
- StartType::CZS => write!(f, "CZS"),
- }
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ StartType::CPS(n) => write!(f, "CPS{}", n),
+ StartType::FCS => write!(f, "FCS"),
+ StartType::FES => write!(f, "FES"),
+ StartType::TPS => write!(f, "TPS"),
+ StartType::CZS => write!(f, "CZS"),
+ }
+ }
}
impl StartType {
- fn player_turnorder_at(&self, ply: usize) -> (Player, TurnOrder) {
- match self {
- StartType::CPS(c) => {
- if ply / 2 >= (*c as usize) {
- if ply % 2 == 0 {
- (Player::White, TurnOrder::Normal)
- } else {
- (Player::Black, TurnOrder::Normal)
- }
- } else {
- if ply % 2 == 0 {
- (Player::White, TurnOrder::WhitePlacesBlack)
- } else {
- (Player::Black, TurnOrder::BlackPlacesWhite)
- }
- }
- }
- StartType::FCS => StartType::CPS(1).player_turnorder_at(ply),
- StartType::FES => StartType::CPS(1).player_turnorder_at(ply),
- StartType::TPS => match ply {
- 0 => (Player::White, TurnOrder::WhitePlacesBlack),
- 1 => (Player::White, TurnOrder::WhitePlacesBlack),
- 2 => (Player::Black, TurnOrder::BlackPlacesWhite),
- n => (
- if n % 2 == 0 {
- Player::Black
- } else {
- Player::White
- },
- TurnOrder::Normal,
- ),
- },
- StartType::CZS => match ply {
- 0 => (Player::White, TurnOrder::WhitePlacesBlack),
- n => (
- if n % 2 == 1 {
- Player::White
- } else {
- Player::Black
- },
- TurnOrder::Normal,
- ),
- },
- }
- }
+ fn player_turnorder_at(&self, ply: usize) -> (Player, TurnOrder) {
+ match self {
+ StartType::CPS(c) => {
+ if ply / 2 >= (*c as usize) {
+ if ply % 2 == 0 {
+ (Player::White, TurnOrder::Normal)
+ } else {
+ (Player::Black, TurnOrder::Normal)
+ }
+ } else {
+ if ply % 2 == 0 {
+ (Player::White, TurnOrder::WhitePlacesBlack)
+ } else {
+ (Player::Black, TurnOrder::BlackPlacesWhite)
+ }
+ }
+ }
+ StartType::FCS => StartType::CPS(1).player_turnorder_at(ply),
+ StartType::FES => StartType::CPS(1).player_turnorder_at(ply),
+ StartType::TPS => match ply {
+ 0 => (Player::White, TurnOrder::WhitePlacesBlack),
+ 1 => (Player::White, TurnOrder::WhitePlacesBlack),
+ 2 => (Player::Black, TurnOrder::BlackPlacesWhite),
+ n => (
+ if n % 2 == 0 {
+ Player::Black
+ } else {
+ Player::White
+ },
+ TurnOrder::Normal,
+ ),
+ },
+ StartType::CZS => match ply {
+ 0 => (Player::White, TurnOrder::WhitePlacesBlack),
+ n => (
+ if n % 2 == 1 {
+ Player::White
+ } else {
+ Player::Black
+ },
+ TurnOrder::Normal,
+ ),
+ },
+ }
+ }
}
pub struct Game {
- size: u8,
- start_type: StartType,
- current_player: Player,
- turn_order: TurnOrder,
- actions: Vec,
- states: Vec,
- white_player_name: String,
- black_player_name: String,
- date_string: String,
- win_type: Option,
+ size: u8,
+ start_type: StartType,
+ current_player: Player,
+ turn_order: TurnOrder,
+ actions: Vec,
+ states: Vec,
+ white_player_name: String,
+ black_player_name: String,
+ date_string: String,
+ win_type: Option,
}
impl Game {
- pub fn new(
- size: u8,
- white_player_name: &str,
- black_player_name: &str,
- date_str: &str,
- start_type: StartType,
- ) -> (Game, String) {
- let (size, warning) = if (size <= 3) || (size >= 8) {
- (5, format!("Warning: the requested game size of {}x{} is not supported, defaulting to 5x5.",
- size, size))
- } else {
- (size, String::new())
- };
-
- let (current_player, turn_order) = start_type.player_turnorder_at(0);
- (
- Game {
- size,
- start_type,
- states: vec![GameState::new(size)],
- actions: Vec::new(),
- current_player,
- turn_order,
- win_type: None,
- white_player_name: white_player_name.to_string(),
- black_player_name: black_player_name.to_string(),
- date_string: date_str.to_string(),
- },
- warning,
- )
- }
-
- pub fn query_square(&self, pos: &Position) -> Option<&Stack> {
- self.last_state().query_pos(pos)
- }
-
- pub fn get_current_player_name(&self) -> &str {
- match self.current_player {
- Player::Black => &self.black_player_name,
- Player::White => &self.white_player_name,
- }
- }
-
- pub fn get_pieces(&self, player: Player, stone: Stone) -> u8 {
- self.last_state().remaining_pieces(player, stone)
- }
-
- pub fn get_current_player(&self) -> Player {
- self.current_player
- }
-
- pub fn get_stone_owner(&self) -> Player {
- match self.turn_order {
- TurnOrder::WhitePlacesBlack => Player::Black,
- TurnOrder::BlackPlacesWhite => Player::White,
- TurnOrder::Normal => self.current_player,
- }
- }
-
- pub fn get_start_type(&self) -> StartType {
- self.start_type
- }
-
- pub fn get_size(&self) -> u8 {
- self.size
- }
-
- pub fn get_ply(&self) -> usize {
- self.states.len() - 1
- }
-
- // Relying on only ::new(...) being used to make instances
- fn last_state(&self) -> &GameState {
- &self.states[self.states.len() - 1]
- }
-
- pub fn get_date_string(&self) -> &str {
- &self.date_string
- }
-
- pub fn get_action_lines(&self) -> Vec {
- let mut result = Vec::new();
- let mut newline = true;
- let num_actions = self.actions.len();
- if num_actions > 0 {
- 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?
- line += &format!("{}. ", i / 2 + 1);
- }
- line += &format!("{} ", self.actions[i].to_string());
- if i > 0 && !newline {
- result.push(line);
- line = String::new();
- }
- newline = !newline;
- }
- if !line.is_empty() {
- result.push(line);
- }
- }
- if self.win_type.is_some() {
- result.push(self.win_type.unwrap().to_string());
- }
- result
- }
-
- pub fn perform_action(
- &mut self,
- act: Action,
- ) -> Result<(Vec, Option), String> {
- let maybe_state = self.states.last();
- if maybe_state.is_none() {
- return Err(String::from("Internal error: cannot find last game state"));
- }
- let state = maybe_state.unwrap();
- let new_state_either = match &act {
- Action::Place(player, pos, stone) => match self.turn_order {
- TurnOrder::WhitePlacesBlack => {
- if (self.current_player == Player::White)
- && (*player == Player::Black)
- && (*stone == Stone::Flat)
- {
- state.place_stone(*player, pos, *stone, Some(self.start_type))
- } else {
- Err(format!(
- "At the start of the game in {}, W must place a B flat.",
- self.start_type
- ))
- }
- }
- TurnOrder::BlackPlacesWhite => {
- if (self.current_player == Player::Black)
- && (*player == Player::White)
- && (*stone == Stone::Flat)
- {
- state.place_stone(*player, pos, *stone, None)
- } else {
- Err(format!(
- "At the start of the game in {}, B must place a W flat.",
- self.start_type
- ))
- }
- }
- TurnOrder::Normal => state.place_stone(*player, pos, *stone, None),
- },
- Action::Move(pos, direction, picked_up, drops) => match self.turn_order {
- TurnOrder::Normal => {
- state.move_stack(self.current_player, pos, *direction, *picked_up, drops)
- }
- _ => Err(format!(
- "At the start of the game in {} only placing flats is allowed.",
- self.start_type
- )),
- },
- };
-
- match new_state_either {
- Err(e) => return Err(e),
- Ok((pos_vec, new_state)) => {
- self.states.push(new_state);
- self.actions.push(act);
- let (current_player, turn_order) =
- self.start_type.player_turnorder_at(self.get_ply());
- self.current_player = current_player;
- self.turn_order = turn_order;
- // This is safe as we have just added to the states vector
- self.win_type = self.last_state().check_win();
- return Ok((pos_vec, self.win_type));
- }
- }
- }
-
- pub fn undo(&mut self) -> Result, String> {
- if self.actions.len() > 0 {
- self.actions.pop();
- self.states.pop();
- let (current_player, turn_order) = self.start_type.player_turnorder_at(self.get_ply());
- self.turn_order = turn_order;
- self.current_player = current_player;
- // I'm too lazy to work out exactly which squares must be
- // redrawn when an action is undone
- let mut redraw_all: Vec = 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!"))
- }
- }
+ pub fn new(
+ size: u8,
+ white_player_name: &str,
+ black_player_name: &str,
+ date_str: &str,
+ start_type: StartType,
+ ) -> (Game, String) {
+ let (size, warning) = if (size <= 3) || (size >= 8) {
+ (
+ 5,
+ format!(
+ "Warning: the requested game size of {}x{} is not supported, defaulting to 5x5.",
+ size, size
+ ),
+ )
+ } else {
+ (size, String::new())
+ };
+
+ let (current_player, turn_order) = start_type.player_turnorder_at(0);
+ (
+ Game {
+ size,
+ start_type,
+ states: vec![GameState::new(size)],
+ actions: Vec::new(),
+ current_player,
+ turn_order,
+ win_type: None,
+ white_player_name: white_player_name.to_string(),
+ black_player_name: black_player_name.to_string(),
+ date_string: date_str.to_string(),
+ },
+ warning,
+ )
+ }
+
+ pub fn query_square(&self, pos: &Position) -> Option<&Stack> {
+ self.last_state().query_pos(pos)
+ }
+
+ pub fn get_current_player_name(&self) -> &str {
+ match self.current_player {
+ Player::Black => &self.black_player_name,
+ Player::White => &self.white_player_name,
+ }
+ }
+
+ pub fn get_pieces(&self, player: Player, stone: Stone) -> u8 {
+ self.last_state().remaining_pieces(player, stone)
+ }
+
+ pub fn get_current_player(&self) -> Player {
+ self.current_player
+ }
+
+ pub fn get_stone_owner(&self) -> Player {
+ match self.turn_order {
+ TurnOrder::WhitePlacesBlack => Player::Black,
+ TurnOrder::BlackPlacesWhite => Player::White,
+ TurnOrder::Normal => self.current_player,
+ }
+ }
+
+ pub fn get_start_type(&self) -> StartType {
+ self.start_type
+ }
+
+ pub fn get_size(&self) -> u8 {
+ self.size
+ }
+
+ pub fn get_ply(&self) -> usize {
+ self.states.len() - 1
+ }
+
+ // Relying on only ::new(...) being used to make instances
+ fn last_state(&self) -> &GameState {
+ &self.states[self.states.len() - 1]
+ }
+
+ pub fn get_date_string(&self) -> &str {
+ &self.date_string
+ }
+
+ pub fn get_action_lines(&self) -> Vec {
+ let mut result = Vec::new();
+ let mut newline = true;
+ let num_actions = self.actions.len();
+ if num_actions > 0 {
+ 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?
+ line += &format!("{}. ", i / 2 + 1);
+ }
+ line += &format!("{} ", self.actions[i].to_string());
+ if i > 0 && !newline {
+ result.push(line);
+ line = String::new();
+ }
+ newline = !newline;
+ }
+ if !line.is_empty() {
+ result.push(line);
+ }
+ }
+ if self.win_type.is_some() {
+ result.push(self.win_type.unwrap().to_string());
+ }
+ result
+ }
+
+ pub fn perform_action(
+ &mut self,
+ act: Action,
+ ) -> Result<(Vec, Option), String> {
+ let maybe_state = self.states.last();
+ if maybe_state.is_none() {
+ return Err(String::from("Internal error: cannot find last game state"));
+ }
+ let state = maybe_state.unwrap();
+ let new_state_either = match &act {
+ Action::Place(player, pos, stone) => match self.turn_order {
+ TurnOrder::WhitePlacesBlack => {
+ if (self.current_player == Player::White)
+ && (*player == Player::Black)
+ && (*stone == Stone::Flat)
+ {
+ state.place_stone(*player, pos, *stone, Some(self.start_type))
+ } else {
+ Err(format!(
+ "At the start of the game in {}, W must place a B flat.",
+ self.start_type
+ ))
+ }
+ }
+ TurnOrder::BlackPlacesWhite => {
+ if (self.current_player == Player::Black)
+ && (*player == Player::White)
+ && (*stone == Stone::Flat)
+ {
+ state.place_stone(*player, pos, *stone, None)
+ } else {
+ Err(format!(
+ "At the start of the game in {}, B must place a W flat.",
+ self.start_type
+ ))
+ }
+ }
+ TurnOrder::Normal => state.place_stone(*player, pos, *stone, None),
+ },
+ Action::Move(pos, direction, picked_up, drops) => match self.turn_order {
+ TurnOrder::Normal => {
+ state.move_stack(self.current_player, pos, *direction, *picked_up, drops)
+ }
+ _ => Err(format!(
+ "At the start of the game in {} only placing flats is allowed.",
+ self.start_type
+ )),
+ },
+ };
+
+ match new_state_either {
+ Err(e) => return Err(e),
+ Ok((pos_vec, new_state)) => {
+ self.states.push(new_state);
+ self.actions.push(act);
+ let (current_player, turn_order) = self.start_type.player_turnorder_at(self.get_ply());
+ self.current_player = current_player;
+ self.turn_order = turn_order;
+ // This is safe as we have just added to the states vector
+ self.win_type = self.last_state().check_win();
+ return Ok((pos_vec, self.win_type));
+ }
+ }
+ }
+
+ pub fn undo(&mut self) -> Result, String> {
+ if self.actions.len() > 0 {
+ self.actions.pop();
+ self.states.pop();
+ let (current_player, turn_order) = self.start_type.player_turnorder_at(self.get_ply());
+ self.turn_order = turn_order;
+ self.current_player = current_player;
+ // I'm too lazy to work out exactly which squares must be
+ // redrawn when an action is undone
+ let mut redraw_all: Vec = 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 {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(
- f,
- "[Date \"{}\"]\n[Player1 \"{}\"]\n[Player2 \"{}\"]\n[Size \"{}\"]\n[Variant \"{}\"]\n{}",
- self.date_string,
- self.white_player_name,
- self.black_player_name,
- self.size,
- self.start_type,
- self.get_action_lines().join("\n"),
- )
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "[Date \"{}\"]\n[Player1 \"{}\"]\n[Player2 \"{}\"]\n[Size \"{}\"]\n[Variant \"{}\"]\n{}",
+ self.date_string,
+ self.white_player_name,
+ self.black_player_name,
+ self.size,
+ self.start_type,
+ self.get_action_lines().join("\n"),
+ )
+ }
}
diff --git a/src/gui/boardwidget.rs b/src/gui/boardwidget.rs
index d831ed1..2708f21 100644
--- a/src/gui/boardwidget.rs
+++ b/src/gui/boardwidget.rs
@@ -1,18 +1,18 @@
/*
- This file is part of Takwrap.
+ 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.
+ 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.
- Takwrap 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.
+ Takwrap 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 Takwrap. If not, see .
+ You should have received a copy of the GNU General Public License
+ along with Takwrap. If not, see .
*/
use fltk::{app::*, button::*, draw::*};
@@ -25,240 +25,240 @@ use crate::game::*;
// when rendering in button.draw2(...) later. We do, however, need to
// use compute_square_size because the widget may be resized.
struct BoardRenderer {
- even_square_colour: Color,
- odd_square_colour: Color,
- black_colour: Color,
- white_colour: Color,
- stone_size: f32,
- game_size: i32,
- frame_pad: i32,
- xorg: i32,
- yorg: i32,
+ even_square_colour: Color,
+ odd_square_colour: Color,
+ black_colour: Color,
+ white_colour: Color,
+ stone_size: f32,
+ game_size: i32,
+ frame_pad: i32,
+ xorg: i32,
+ yorg: i32,
}
impl BoardRenderer {
- fn default() -> BoardRenderer {
- BoardRenderer {
- // Sandy brown
- odd_square_colour: Color::from_rgb(244, 164, 96),
- // Moccasin
- even_square_colour: Color::from_rgb(255, 228, 181),
- // Colours for pieces
- black_colour: Color::from_rgb(178, 34, 34),
- white_colour: Color::from_rgb(250, 235, 215),
- // Gap between stones and edged of square
- stone_size: 0.8,
- // Defaults
- game_size: 5,
- frame_pad: 5,
- xorg: 0,
- yorg: 0,
- }
- }
-
- fn with_size(mut self, size: i32) -> BoardRenderer {
- self.game_size = size;
- return self;
- }
-
- fn with_org(mut self, button: &Button) -> BoardRenderer {
- let x0 = button.x();
- let y0 = button.y();
- self.xorg = x0 + self.frame_pad;
- self.yorg = y0 + self.frame_pad;
- return self;
- }
-
- fn compute_square_size(&self, button: &Button) -> i32 {
- let board_size = i32::min(button.width(), button.height()) - self.frame_pad * 2;
- return board_size / self.game_size;
- }
-
- fn render_stone(
- &self,
- square_size: i32,
- player: Player,
- stone: Stone,
- row: i32,
- col: i32,
- shift: i32,
- ) {
- set_draw_color(match player {
- Player::Black => self.black_colour,
- Player::White => self.white_colour,
- });
-
- // Gap scales with square size
- let stone_gap = (square_size as f32 * (1.00 - self.stone_size)).round() as i32;
- let d = square_size - stone_gap * 2;
- let shift = (shift * d) / 8;
- let x1 = self.xorg + col * square_size + stone_gap + shift;
- let y1 = self.yorg + row * square_size + stone_gap - shift;
-
- // Outline with thickness varying by stone size
- let setup_border_pen = || {
- set_line_style(LineStyle::Solid, i32::max(1, d / 24));
- set_draw_color(Color::Black);
- };
-
- match stone {
- Stone::Capstone => {
- draw_pie(x1, y1, d, d, 0.0, 360.0);
- setup_border_pen();
- let r = (d as f64) / 2.0;
- draw_circle(r + x1 as f64, r + y1 as f64, r);
- }
- Stone::Flat => {
- draw_rectf(x1, y1, d, d);
- setup_border_pen();
- draw_rect_with_color(x1, y1, d, d, Color::Black);
- }
- Stone::Standing => {
- // These look bad shifted so we shift it back if was
- let df = d as f64;
- // and we have to fudge it a little? Some rounding things presumably
- let ds = if shift > 0 { df / 8.0 - 0.5 } else { 0.0 };
- let x1 = x1 as f64 - ds;
- let y1 = y1 as f64 + ds;
-
- let verts = [
- (x1 + df * 0.3, y1 + df * 0.075),
- (x1 + df * 0.925, y1 + df * 0.7),
- (x1 + df * 0.7, y1 + df * 0.925),
- (x1 + df * 0.075, y1 + df * 0.3),
- ];
-
- begin_polygon();
- for (x, y) in verts.iter() {
- vertex(*x, *y)
- }
- end_polygon();
-
- setup_border_pen();
- begin_loop();
- for (x, y) in verts.iter() {
- vertex(*x, *y);
- }
- end_loop();
- }
- }
- set_line_style(LineStyle::Solid, 1);
- }
-
- fn render_icons(&self, square_size: i32, player: Player, row: i32, col: i32, shift: i32) {
- let stone_gap = square_size as f32 * (1.00 - self.stone_size);
- let icon_gap = (stone_gap * 0.15).round() as i32;
- let icon_width = (stone_gap * 0.7).round() as i32;
- let icon_height = icon_width / 2;
-
- let x1 = self.xorg + col * square_size + icon_gap;
- let y1 = self.yorg + (row + 1) * square_size - icon_gap;
-
- set_line_style(LineStyle::Solid, i32::max(1, icon_width / 8));
- set_draw_color(Color::Black);
-
- set_draw_color(match player {
- Player::Black => self.black_colour,
- Player::White => self.white_colour,
- });
- let y2 = y1 - icon_height * (1 + shift as i32);
- draw_rectf(x1, y2, icon_width, icon_height);
- draw_rect_with_color(x1, y2, icon_width, icon_height, Color::Black);
-
- set_line_style(LineStyle::Solid, 1);
- }
-
- fn render_stack(&self, square_size: i32, row: i32, col: i32, stack: &Stack) {
- // Is there a better way than usize::min((...), i32::MAX as usize) as i32 ?
- let cutoff = stack.len() as i32 - self.game_size;
-
- for (shift, piece) in stack.iter().enumerate() {
- let shift = shift as i32; // TODO: please don't overflow
- if shift < cutoff {
- self.render_icons(square_size, piece.player, row, col, shift);
- } else {
- self.render_stone(
- square_size,
- piece.player,
- piece.stone,
- row,
- col,
- shift - i32::max(0, cutoff),
- );
- }
- }
- }
-
- fn render_board(&self, square_size: i32, board: &Vec) {
- let mut x = 0;
- let mut y = 0;
- for stack in board.iter() {
- self.render_stack(square_size, x, y, stack);
- x += 1;
- if x + 1 >= self.game_size {
- x = 0;
- y += 1;
- if y + 1 >= self.game_size {
- panic!("Game size mismatch between renderer and internal state.");
- }
- }
- }
- }
-
- fn render_background(&self, square_size: i32) {
- for x in 0..self.game_size {
- for y in 0..self.game_size {
- if (x + y) % 2 == 0 {
- set_draw_color(self.even_square_colour);
- } else {
- set_draw_color(self.odd_square_colour);
- }
- draw_rectf(
- self.xorg + x * square_size,
- self.yorg + y * square_size,
- square_size,
- square_size,
- );
- }
- }
- }
+ fn default() -> BoardRenderer {
+ BoardRenderer {
+ // Sandy brown
+ odd_square_colour: Color::from_rgb(244, 164, 96),
+ // Moccasin
+ even_square_colour: Color::from_rgb(255, 228, 181),
+ // Colours for pieces
+ black_colour: Color::from_rgb(178, 34, 34),
+ white_colour: Color::from_rgb(250, 235, 215),
+ // Gap between stones and edged of square
+ stone_size: 0.8,
+ // Defaults
+ game_size: 5,
+ frame_pad: 5,
+ xorg: 0,
+ yorg: 0,
+ }
+ }
+
+ fn with_size(mut self, size: i32) -> BoardRenderer {
+ self.game_size = size;
+ return self;
+ }
+
+ fn with_org(mut self, button: &Button) -> BoardRenderer {
+ let x0 = button.x();
+ let y0 = button.y();
+ self.xorg = x0 + self.frame_pad;
+ self.yorg = y0 + self.frame_pad;
+ return self;
+ }
+
+ fn compute_square_size(&self, button: &Button) -> i32 {
+ let board_size = i32::min(button.width(), button.height()) - self.frame_pad * 2;
+ return board_size / self.game_size;
+ }
+
+ fn render_stone(
+ &self,
+ square_size: i32,
+ player: Player,
+ stone: Stone,
+ row: i32,
+ col: i32,
+ shift: i32,
+ ) {
+ set_draw_color(match player {
+ Player::Black => self.black_colour,
+ Player::White => self.white_colour,
+ });
+
+ // Gap scales with square size
+ let stone_gap = (square_size as f32 * (1.00 - self.stone_size)).round() as i32;
+ let d = square_size - stone_gap * 2;
+ let shift = (shift * d) / 8;
+ let x1 = self.xorg + col * square_size + stone_gap + shift;
+ let y1 = self.yorg + row * square_size + stone_gap - shift;
+
+ // Outline with thickness varying by stone size
+ let setup_border_pen = || {
+ set_line_style(LineStyle::Solid, i32::max(1, d / 24));
+ set_draw_color(Color::Black);
+ };
+
+ match stone {
+ Stone::Capstone => {
+ draw_pie(x1, y1, d, d, 0.0, 360.0);
+ setup_border_pen();
+ let r = (d as f64) / 2.0;
+ draw_circle(r + x1 as f64, r + y1 as f64, r);
+ }
+ Stone::Flat => {
+ draw_rectf(x1, y1, d, d);
+ setup_border_pen();
+ draw_rect_with_color(x1, y1, d, d, Color::Black);
+ }
+ Stone::Standing => {
+ // These look bad shifted so we shift it back if was
+ let df = d as f64;
+ // and we have to fudge it a little? Some rounding things presumably
+ let ds = if shift > 0 { df / 8.0 - 0.5 } else { 0.0 };
+ let x1 = x1 as f64 - ds;
+ let y1 = y1 as f64 + ds;
+
+ let verts = [
+ (x1 + df * 0.3, y1 + df * 0.075),
+ (x1 + df * 0.925, y1 + df * 0.7),
+ (x1 + df * 0.7, y1 + df * 0.925),
+ (x1 + df * 0.075, y1 + df * 0.3),
+ ];
+
+ begin_polygon();
+ for (x, y) in verts.iter() {
+ vertex(*x, *y)
+ }
+ end_polygon();
+
+ setup_border_pen();
+ begin_loop();
+ for (x, y) in verts.iter() {
+ vertex(*x, *y);
+ }
+ end_loop();
+ }
+ }
+ set_line_style(LineStyle::Solid, 1);
+ }
+
+ fn render_icons(&self, square_size: i32, player: Player, row: i32, col: i32, shift: i32) {
+ let stone_gap = square_size as f32 * (1.00 - self.stone_size);
+ let icon_gap = (stone_gap * 0.15).round() as i32;
+ let icon_width = (stone_gap * 0.7).round() as i32;
+ let icon_height = icon_width / 2;
+
+ let x1 = self.xorg + col * square_size + icon_gap;
+ let y1 = self.yorg + (row + 1) * square_size - icon_gap;
+
+ set_line_style(LineStyle::Solid, i32::max(1, icon_width / 8));
+ set_draw_color(Color::Black);
+
+ set_draw_color(match player {
+ Player::Black => self.black_colour,
+ Player::White => self.white_colour,
+ });
+ let y2 = y1 - icon_height * (1 + shift as i32);
+ draw_rectf(x1, y2, icon_width, icon_height);
+ draw_rect_with_color(x1, y2, icon_width, icon_height, Color::Black);
+
+ set_line_style(LineStyle::Solid, 1);
+ }
+
+ fn render_stack(&self, square_size: i32, row: i32, col: i32, stack: &Stack) {
+ // Is there a better way than usize::min((...), i32::MAX as usize) as i32 ?
+ let cutoff = stack.len() as i32 - self.game_size;
+
+ for (shift, piece) in stack.iter().enumerate() {
+ let shift = shift as i32; // TODO: please don't overflow
+ if shift < cutoff {
+ self.render_icons(square_size, piece.player, row, col, shift);
+ } else {
+ self.render_stone(
+ square_size,
+ piece.player,
+ piece.stone,
+ row,
+ col,
+ shift - i32::max(0, cutoff),
+ );
+ }
+ }
+ }
+
+ fn render_board(&self, square_size: i32, board: &Vec) {
+ let mut x = 0;
+ let mut y = 0;
+ for stack in board.iter() {
+ self.render_stack(square_size, x, y, stack);
+ x += 1;
+ if x + 1 >= self.game_size {
+ x = 0;
+ y += 1;
+ if y + 1 >= self.game_size {
+ panic!("Game size mismatch between renderer and internal state.");
+ }
+ }
+ }
+ }
+
+ fn render_background(&self, square_size: i32) {
+ for x in 0..self.game_size {
+ for y in 0..self.game_size {
+ if (x + y) % 2 == 0 {
+ set_draw_color(self.even_square_colour);
+ } else {
+ set_draw_color(self.odd_square_colour);
+ }
+ draw_rectf(
+ self.xorg + x * square_size,
+ self.yorg + y * square_size,
+ square_size,
+ square_size,
+ );
+ }
+ }
+ }
}
pub struct BoardWidget {
- button: Button,
- renderer: Rc,
+ button: Button,
+ renderer: Rc,
}
impl BoardWidget {
- pub fn new(button: Button, size: i32) -> BoardWidget {
- let renderer = Rc::new(BoardRenderer::default().with_size(size).with_org(&button));
- let mut g = BoardWidget { button, renderer };
- g.button.draw2(g.generate_draw(&Vec::new()));
- return g;
- }
-
- pub fn to_square(&self, x: i32, y: i32) -> (i32, i32) {
- let square_size = self.renderer.compute_square_size(&self.button);
-
- let x = x - self.renderer.xorg;
- let y = y - self.renderer.yorg;
-
- (x / square_size, y / square_size)
- }
-
- pub fn generate_draw(&self, board: &Vec) -> impl FnMut(&mut Button) {
- let renderer = self.renderer.clone();
- let board = board.clone();
-
- move |button: &mut Button| {
- let square_size = renderer.compute_square_size(&button);
- renderer.render_background(square_size);
- renderer.render_board(square_size, &board);
-
- // // Test code for now
- // renderer.render_stack(square_size, 1, 4, &stack);
- // renderer.render_stone(square_size, Player::Black, Stone::Capstone, 1, 2, 0);
- // renderer.render_stone(square_size, Player::Black, Stone::Standing, 2, 2, 0);
- }
- }
+ pub fn new(button: Button, size: i32) -> BoardWidget {
+ let renderer = Rc::new(BoardRenderer::default().with_size(size).with_org(&button));
+ let mut g = BoardWidget { button, renderer };
+ g.button.draw2(g.generate_draw(&Vec::new()));
+ return g;
+ }
+
+ pub fn to_square(&self, x: i32, y: i32) -> (i32, i32) {
+ let square_size = self.renderer.compute_square_size(&self.button);
+
+ let x = x - self.renderer.xorg;
+ let y = y - self.renderer.yorg;
+
+ (x / square_size, y / square_size)
+ }
+
+ pub fn generate_draw(&self, board: &Vec) -> impl FnMut(&mut Button) {
+ let renderer = self.renderer.clone();
+ let board = board.clone();
+
+ move |button: &mut Button| {
+ let square_size = renderer.compute_square_size(&button);
+ renderer.render_background(square_size);
+ renderer.render_board(square_size, &board);
+
+ // // Test code for now
+ // renderer.render_stack(square_size, 1, 4, &stack);
+ // renderer.render_stone(square_size, Player::Black, Stone::Capstone, 1, 2, 0);
+ // renderer.render_stone(square_size, Player::Black, Stone::Standing, 2, 2, 0);
+ }
+ }
}
diff --git a/src/gui/mod.rs b/src/gui/mod.rs
index 47f56d5..1980fdb 100644
--- a/src/gui/mod.rs
+++ b/src/gui/mod.rs
@@ -1,18 +1,18 @@
/*
- This file is part of Takwrap.
+ 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.
+ 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.
- Takwrap 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.
+ Takwrap 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 Takwrap. If not, see .
+ You should have received a copy of the GNU General Public License
+ along with Takwrap. If not, see .
*/
mod boardwidget;
@@ -22,101 +22,101 @@ use fltk::{app::*, button::*, menu::*, text::*, window::*};
#[derive(Clone, Copy)]
enum GUIMessage {
- None,
- Test,
- BoardClick,
+ None,
+ Test,
+ BoardClick,
}
pub struct GUI {
- app: App,
- window: DoubleWindow,
- board: BoardWidget,
- log: TextDisplay,
- sender: Sender,
- receiver: Receiver,
+ app: App,
+ window: DoubleWindow,
+ board: BoardWidget,
+ log: TextDisplay,
+ sender: Sender,
+ receiver: Receiver,
}
impl GUI {
- pub fn new() -> GUI {
- let app = App::default();
-
- // TODO: Why is this not the same as
- /*
- let mut window = Window::default()
- .with_size(800, 600)
- .with_label("Takwrap")
- .center_screen();
- */
- // Windows made that way are not resizable?
-
- let mut window = DoubleWindow::new(0, 0, 800, 600, "Takwrap").center_screen();
-
- let (sender, receiver) = channel::();
-
- const MENU_HEIGHT: i32 = 24;
- let mut menu = SysMenuBar::default().with_size(800, MENU_HEIGHT);
- // menu.set_text_font(Font::Helvetica);
- menu.set_color(Color::Light2);
- menu.add_emit(
- "&File/New...\t",
- Shortcut::None,
- MenuFlag::Normal,
- sender,
- GUIMessage::None,
- );
- menu.add_emit(
- "&File/Open...\t",
- Shortcut::None,
- MenuFlag::Normal,
- sender,
- GUIMessage::Test,
- );
-
- const LOG_WIDTH: i32 = 800 - 600 + MENU_HEIGHT;
- let mut log = TextDisplay::default()
- .with_size(LOG_WIDTH, 600 - menu.height())
- .with_pos(800 - LOG_WIDTH, menu.height());
- log.set_buffer(Some(TextBuffer::default()));
- log.insert("test\n");
-
- let mut board_button = Button::default()
- .with_size(800 - LOG_WIDTH, 600 - menu.height())
- .below_of(&menu, 0);
- board_button.set_frame(FrameType::EmbossedBox);
- board_button.emit(sender, GUIMessage::BoardClick);
-
- window.resizable(&mut board_button);
- window.end();
- window.show();
-
- let board = BoardWidget::new(board_button, 5);
-
- // button.emit(sender, GUIMessage::Foo);
-
- GUI {
- app,
- board,
- window,
- log,
- sender,
- receiver,
- }
- }
-
- pub fn run(&mut self) -> Result<(), fltk::prelude::FltkError> {
- while self.app.wait() {
- if let Some(msg) = self.receiver.recv() {
- match msg {
- GUIMessage::BoardClick => {
- let x = event_x();
- let y = event_y();
- let (x, y) = self.board.to_square(x, y);
- self.log.insert(&format!("Board click ({},{})\n", x, y));
- }
- _ => (),
- }
- }
- }
- Ok(())
- }
+ pub fn new() -> GUI {
+ let app = App::default();
+
+ // TODO: Why is this not the same as
+ /*
+ let mut window = Window::default()
+ .with_size(800, 600)
+ .with_label("Takwrap")
+ .center_screen();
+ */
+ // Windows made that way are not resizable?
+
+ let mut window = DoubleWindow::new(0, 0, 800, 600, "Takwrap").center_screen();
+
+ let (sender, receiver) = channel::();
+
+ const MENU_HEIGHT: i32 = 24;
+ let mut menu = SysMenuBar::default().with_size(800, MENU_HEIGHT);
+ // menu.set_text_font(Font::Helvetica);
+ menu.set_color(Color::Light2);
+ menu.add_emit(
+ "&File/New...\t",
+ Shortcut::None,
+ MenuFlag::Normal,
+ sender,
+ GUIMessage::None,
+ );
+ menu.add_emit(
+ "&File/Open...\t",
+ Shortcut::None,
+ MenuFlag::Normal,
+ sender,
+ GUIMessage::Test,
+ );
+
+ const LOG_WIDTH: i32 = 800 - 600 + MENU_HEIGHT;
+ let mut log = TextDisplay::default()
+ .with_size(LOG_WIDTH, 600 - menu.height())
+ .with_pos(800 - LOG_WIDTH, menu.height());
+ log.set_buffer(Some(TextBuffer::default()));
+ log.insert("test\n");
+
+ let mut board_button = Button::default()
+ .with_size(800 - LOG_WIDTH, 600 - menu.height())
+ .below_of(&menu, 0);
+ board_button.set_frame(FrameType::EmbossedBox);
+ board_button.emit(sender, GUIMessage::BoardClick);
+
+ window.resizable(&mut board_button);
+ window.end();
+ window.show();
+
+ let board = BoardWidget::new(board_button, 5);
+
+ // button.emit(sender, GUIMessage::Foo);
+
+ GUI {
+ app,
+ board,
+ window,
+ log,
+ sender,
+ receiver,
+ }
+ }
+
+ pub fn run(&mut self) -> Result<(), fltk::prelude::FltkError> {
+ while self.app.wait() {
+ if let Some(msg) = self.receiver.recv() {
+ match msg {
+ GUIMessage::BoardClick => {
+ let x = event_x();
+ let y = event_y();
+ let (x, y) = self.board.to_square(x, y);
+ self.log.insert(&format!("Board click ({},{})\n", x, y));
+ }
+ _ => (),
+ }
+ }
+ }
+ Ok(())
+ }
}
diff --git a/src/main.rs b/src/main.rs
index 6045d0f..1d62045 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -38,161 +38,162 @@ use std::env;
use std::time::SystemTime;
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 terminal = false;
- let mut start_type_string = String::new();
-
- println!("Takwrap, a TUI for playing Tak and interacting with Tak engines.\n
+ 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 terminal = false;
+ let mut start_type_string = String::new();
+
+ println!("Takwrap, a TUI for playing Tak and interacting with Tak engines.\n
Copyright (C) 2020, tslil clingman\n
This program comes with ABSOLUTELY NO WARRANTY; and is made available under the terms of the GNU GPL v3 license. This is free software, and you are welcome to redistribute it under certain conditions; see COPYING for details.\n"
);
- {
- let mut ap = ArgumentParser::new();
- ap.set_description("");
+ {
+ let mut ap = ArgumentParser::new();
+ ap.set_description("");
- ap.refer(&mut p1_name).add_option(
- &["--player1"],
- Store,
- "Name of first player, defaults to $USER. See --name-white
+ 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
+ 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 start_type_string)
- .add_option(
- &["-v", "--variant-start"],
- Store,
- "Select the rule variant for
+ ap.refer(&mut start_type_string)
+ .add_option(
+ &["-v", "--variant-start"],
+ Store,
+ "Select the rule variant for
the start of play. Valid choices are CPSn, FCS, FES, TPS,
and CZS. The default starting type, which matches the
original rules, is CPS1. See ENGINE below for a warning on
this.",
- )
- .metavar("VRNT");
+ )
+ .metavar("VRNT");
- ap.refer(&mut engine).add_option(
- &["-e", "--engine"],
- Store,
- "Name of process which will be used as a computer
+ 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. Note: ensure that your engine understands the
[Variant XXX] directive if you choose a variant (above).",
- );
+ );
- ap.refer(&mut name)
- .add_option(
- &["--name-white"],
- Store,
- "Force the player named NAME to play white. If not
+ 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.refer(&mut terminal).add_option(
- &["-t", "--terminal"],
- Store,
- "Use the terminal user interface instead of the graphical one.",
- );
-
- ap.parse_args_or_exit();
- }
-
- let start_type;
- if !start_type_string.is_empty() {
- match parse_start_type(&start_type_string) {
- Err(e) => panic!("Invalid start rule variant provided: {}", e),
- Ok(st) => start_type = st,
- }
- } else {
- start_type = StartType::CPS(1);
- }
-
- let call_proc;
- if !engine.is_empty() {
- p2_name = engine.clone();
- call_proc = true;
- } else {
- call_proc = false;
- }
-
- 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 (only ``{}'' and ``{}'' are).",
- name, p1_name, p2_name
- );
- }
- } else {
- // Very random choice
- 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, start_type)
- } else {
- Game::new(size, &p2_name, &p1_name, &date_string, start_type)
- };
-
- if terminal {
- let mut tui = TUI::new(&game, warning);
-
- 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() {
- tui.message_log
- .log_error(format!("Error in cleaning up temporary files: {}", e));
- }
- }
-
- endwin();
- } else {
- let mut gui = GUI::new();
- gui.run().unwrap();
- }
+ )
+ .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.refer(&mut terminal).add_option(
+ &["-t", "--terminal"],
+ Store,
+ "Use the terminal user interface instead of the graphical one.",
+ );
+
+ ap.parse_args_or_exit();
+ }
+
+ let start_type;
+ if !start_type_string.is_empty() {
+ match parse_start_type(&start_type_string) {
+ Err(e) => panic!("Invalid start rule variant provided: {}", e),
+ Ok(st) => start_type = st,
+ }
+ } else {
+ start_type = StartType::CPS(1);
+ }
+
+ let call_proc;
+ if !engine.is_empty() {
+ p2_name = engine.clone();
+ call_proc = true;
+ } else {
+ call_proc = false;
+ }
+
+ 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 (only ``{}'' and ``{}'' are).",
+ name, p1_name, p2_name
+ );
+ }
+ } else {
+ // Very random choice
+ 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, start_type)
+ } else {
+ Game::new(size, &p2_name, &p1_name, &date_string, start_type)
+ };
+
+ if terminal {
+ let mut tui = TUI::new(&game, warning);
+
+ 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() {
+ tui
+ .message_log
+ .log_error(format!("Error in cleaning up temporary files: {}", e));
+ }
+ }
+
+ endwin();
+ } else {
+ let mut gui = GUI::new();
+ gui.run().unwrap();
+ }
}
diff --git a/src/parser.rs b/src/parser.rs
index abac3dc..1e3ecf4 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -1,231 +1,231 @@
/*
- This file is part of Takwrap.
+ 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.
+ 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.
- Takwrap 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.
+ Takwrap 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 Takwrap. If not, see .
+ You should have received a copy of the GNU General Public License
+ along with Takwrap. If not, see .
*/
use crate::game::*;
fn parse_move(mv: &str) -> Result {
- let mut chars = mv.chars();
-
- let c = chars.next();
- if c.is_none() {
- return Err(String::from("Empty string cannot be parsed."));
- }
-
- let x;
- let picked_up;
- let dc = c.unwrap();
- if (dc >= 'a') && (dc <= 'h') {
- // Optional drop number omitted, assume everything
- x = dc;
- picked_up = 1;
- } else {
- if (dc < '1') || ('9' < dc) {
- return Err(String::from(
- "Moves must begin with a digit in 1-8 indicating the stones picked up.",
- ));
- }
- picked_up = dc.to_digit(10).unwrap() as u8;
-
- let c = chars.next();
- if c.is_none() {
- return Err(String::from("Moves must specify a stack position."));
- }
-
- x = c.unwrap();
- }
-
- if (x < 'a') || (x > 'h') {
- return Err(String::from(
- "Moves must specify a valid position on the game board.",
- ));
- }
- let x: u8 = x as u8 - 'a' as u8;
-
- let c = chars.next();
- if c.is_none() {
- return Err(String::from("Moves must specify a stack position."));
- }
-
- let y = c.unwrap();
- if (y < '1') || (y > '8') {
- return Err(String::from(
- "Moves must specify a position on the game board.",
- ));
- }
- let y: u8 = (y.to_digit(10).unwrap() - 1) as u8;
-
- let c = chars.next();
- if c.is_none() {
- return Err(String::from("Moves must specify a move direction."));
- }
-
- let c = c.unwrap();
- // This is so cool! Rust supports some very nice patterns, and
- // checks for uninitialised variables too!
- let dir;
- match c {
- '+' => dir = Direction::Up,
- '-' => dir = Direction::Down,
- '<' => dir = Direction::Left,
- '>' => dir = Direction::Right,
- _ => return Err(String::from("The valid directions are +,-,<, and >.")),
- }
-
- let mut drops: Vec = Vec::new();
- for d in chars {
- if let Some(u) = d.to_digit(10) {
- if u > 9 {
- return Err(String::from(
- "No more than 8 pieces may be dropped on a given square.",
- ));
- } else {
- drops.push(u as u8)
- }
- } else {
- return Err(String::from(
- "A move must specify a number of pieces dropped for each square.",
- ));
- }
- }
-
- let pos = Position { x: x, y: y };
-
- let sum = drops.iter().map(|&d| d as u32).sum::();
- if sum == 0 {
- // Omitted all the drops, it's a total stack move
- drops.push(picked_up);
- } else if sum != picked_up as u32 {
- return Err(format!(
- "The move called for {} stones, but {} were dropped.",
- picked_up, sum
- ));
- }
-
- Ok(Action::Move(pos, dir, picked_up, drops))
+ let mut chars = mv.chars();
+
+ let c = chars.next();
+ if c.is_none() {
+ return Err(String::from("Empty string cannot be parsed."));
+ }
+
+ let x;
+ let picked_up;
+ let dc = c.unwrap();
+ if (dc >= 'a') && (dc <= 'h') {
+ // Optional drop number omitted, assume everything
+ x = dc;
+ picked_up = 1;
+ } else {
+ if (dc < '1') || ('9' < dc) {
+ return Err(String::from(
+ "Moves must begin with a digit in 1-8 indicating the stones picked up.",
+ ));
+ }
+ picked_up = dc.to_digit(10).unwrap() as u8;
+
+ let c = chars.next();
+ if c.is_none() {
+ return Err(String::from("Moves must specify a stack position."));
+ }
+
+ x = c.unwrap();
+ }
+
+ if (x < 'a') || (x > 'h') {
+ return Err(String::from(
+ "Moves must specify a valid position on the game board.",
+ ));
+ }
+ let x: u8 = x as u8 - 'a' as u8;
+
+ let c = chars.next();
+ if c.is_none() {
+ return Err(String::from("Moves must specify a stack position."));
+ }
+
+ let y = c.unwrap();
+ if (y < '1') || (y > '8') {
+ return Err(String::from(
+ "Moves must specify a position on the game board.",
+ ));
+ }
+ let y: u8 = (y.to_digit(10).unwrap() - 1) as u8;
+
+ let c = chars.next();
+ if c.is_none() {
+ return Err(String::from("Moves must specify a move direction."));
+ }
+
+ let c = c.unwrap();
+ // This is so cool! Rust supports some very nice patterns, and
+ // checks for uninitialised variables too!
+ let dir;
+ match c {
+ '+' => dir = Direction::Up,
+ '-' => dir = Direction::Down,
+ '<' => dir = Direction::Left,
+ '>' => dir = Direction::Right,
+ _ => return Err(String::from("The valid directions are +,-,<, and >.")),
+ }
+
+ let mut drops: Vec = Vec::new();
+ for d in chars {
+ if let Some(u) = d.to_digit(10) {
+ if u > 9 {
+ return Err(String::from(
+ "No more than 8 pieces may be dropped on a given square.",
+ ));
+ } else {
+ drops.push(u as u8)
+ }
+ } else {
+ return Err(String::from(
+ "A move must specify a number of pieces dropped for each square.",
+ ));
+ }
+ }
+
+ let pos = Position { x: x, y: y };
+
+ let sum = drops.iter().map(|&d| d as u32).sum::();
+ if sum == 0 {
+ // Omitted all the drops, it's a total stack move
+ drops.push(picked_up);
+ } else if sum != picked_up as u32 {
+ return Err(format!(
+ "The move called for {} stones, but {} were dropped.",
+ picked_up, sum
+ ));
+ }
+
+ Ok(Action::Move(pos, dir, picked_up, drops))
}
fn parse_place(pl: &str, stone_owner: Player) -> Result {
- let mut chars = pl.chars();
-
- let c = chars.next();
- if c.is_none() {
- return Err(String::from("An action cannot be blank."));
- }
-
- let stone_c = c.unwrap();
-
- let stone;
- let x;
- if (stone_c == 'F') || (stone_c == 'C') || (stone_c == 'S') {
- match stone_c {
- 'C' => stone = Stone::Capstone,
- 'S' => stone = Stone::Standing,
- _ => stone = Stone::Flat,
- }
- let c = chars.next();
- if c.is_none() {
- return Err(String::from(
- "Placements must specify a position on the game board.",
- ));
- }
- x = c.unwrap();
- } else {
- stone = Stone::Flat;
- x = stone_c;
- }
-
- if (x < 'a') || (x > 'h') {
- return Err(if (x >= 'A') && (x <= 'Z') {
- format!("Unrecognised stone type '{}' in placement.", x)
- } else {
- String::from("Placements must specify a valid position on the game board.")
- });
- }
- let x: u8 = x as u8 - 'a' as u8;
-
- let c = chars.next();
- if c.is_none() {
- return Err(String::from(
- "Placements must specify a position on the game board.",
- ));
- }
-
- let y = c.unwrap();
- if (y < '1') || (y > '8') {
- return Err(String::from(
- "Placements must specify a valid position on the game board.",
- ));
- }
- let y: u8 = (y.to_digit(10).unwrap() - 1) as u8;
-
- Ok(Action::Place(stone_owner, Position { x: x, y: y }, stone))
+ let mut chars = pl.chars();
+
+ let c = chars.next();
+ if c.is_none() {
+ return Err(String::from("An action cannot be blank."));
+ }
+
+ let stone_c = c.unwrap();
+
+ let stone;
+ let x;
+ if (stone_c == 'F') || (stone_c == 'C') || (stone_c == 'S') {
+ match stone_c {
+ 'C' => stone = Stone::Capstone,
+ 'S' => stone = Stone::Standing,
+ _ => stone = Stone::Flat,
+ }
+ let c = chars.next();
+ if c.is_none() {
+ return Err(String::from(
+ "Placements must specify a position on the game board.",
+ ));
+ }
+ x = c.unwrap();
+ } else {
+ stone = Stone::Flat;
+ x = stone_c;
+ }
+
+ if (x < 'a') || (x > 'h') {
+ return Err(if (x >= 'A') && (x <= 'Z') {
+ format!("Unrecognised stone type '{}' in placement.", x)
+ } else {
+ String::from("Placements must specify a valid position on the game board.")
+ });
+ }
+ let x: u8 = x as u8 - 'a' as u8;
+
+ let c = chars.next();
+ if c.is_none() {
+ return Err(String::from(
+ "Placements must specify a position on the game board.",
+ ));
+ }
+
+ let y = c.unwrap();
+ if (y < '1') || (y > '8') {
+ return Err(String::from(
+ "Placements must specify a valid position on the game board.",
+ ));
+ }
+ let y: u8 = (y.to_digit(10).unwrap() - 1) as u8;
+
+ Ok(Action::Place(stone_owner, Position { x: x, y: y }, stone))
}
pub fn parse_action(stone_owner: Player, act: &str) -> Result {
- if act
- .chars()
- .any(|c| c == '+' || c == '-' || c == '>' || c == '<')
- {
- parse_move(act)
- } else {
- parse_place(act, stone_owner)
- }
+ if act
+ .chars()
+ .any(|c| c == '+' || c == '-' || c == '>' || c == '<')
+ {
+ parse_move(act)
+ } else {
+ parse_place(act, stone_owner)
+ }
}
pub fn parse_start_type(st: &str) -> Result {
- let st = st.trim().to_uppercase();
+ let st = st.trim().to_uppercase();
- if st == StartType::FCS.to_string() {
- return Ok(StartType::FCS);
- };
+ if st == StartType::FCS.to_string() {
+ return Ok(StartType::FCS);
+ };
- if st == StartType::FES.to_string() {
- return Ok(StartType::FES);
- };
+ if st == StartType::FES.to_string() {
+ return Ok(StartType::FES);
+ };
- if st == StartType::TPS.to_string() {
- return Ok(StartType::TPS);
- };
+ if st == StartType::TPS.to_string() {
+ return Ok(StartType::TPS);
+ };
- if st == StartType::CZS.to_string() {
- return Ok(StartType::CZS);
- };
+ if st == StartType::CZS.to_string() {
+ return Ok(StartType::CZS);
+ };
- if st.len() < 4 {
- return Err(String::from(
- "Expecting CPSn as
+ if st.len() < 4 {
+ return Err(String::from(
+ "Expecting CPSn as
alternatives failed to match, but variant string was too
short.",
- ));
- } else {
- if st[..3] == String::from("CPS") {
- let num = st[3..].parse::();
- match num {
- Err(e) => {
- return Err(format!(
- "Failed to parse number, {}. CPS must be
+ ));
+ } else {
+ if st[..3] == String::from("CPS") {
+ let num = st[3..].parse::();
+ match num {
+ Err(e) => {
+ return Err(format!(
+ "Failed to parse number, {}. CPS must be
immediately followed by a positive integer < 256, e.g., CPS1.",
- e.to_string()
- ))
- }
- Ok(n) => return Ok(StartType::CPS(n)),
- }
- }
- }
-
- return Err(format!("Unknown start type ``{}''", st));
+ e.to_string()
+ ))
+ }
+ Ok(n) => return Ok(StartType::CPS(n)),
+ }
+ }
+ }
+
+ return Err(format!("Unknown start type ``{}''", st));
}
diff --git a/src/tui.rs b/src/tui.rs
index 292fb32..970b8fe 100644
--- a/src/tui.rs
+++ b/src/tui.rs
@@ -1,18 +1,18 @@
/*
- This file is part of Takwrap.
+ 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.
+ 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.
- Takwrap 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.
+ Takwrap 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 Takwrap. If not, see .
+ You should have received a copy of the GNU General Public License
+ along with Takwrap. If not, see .
*/
extern crate pancurses;
@@ -29,779 +29,789 @@ use crate::parser::*;
#[derive(PartialEq)]
pub enum TUIResult {
- Undo,
- Quit,
- Save,
- SwitchElement,
- Raw(String),
+ 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,
+ action_window: Window,
+ pieces_window: Window,
+ has_colours: bool,
+ white_colour: u8,
+ black_colour: u8,
+ num_lines: usize,
+ offset: usize,
+ action_lines: Vec,
}
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 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,
- size: u8,
- cell_height: u8,
- cell_width: u8,
- hoff: i32,
- cur_cell: Position,
- has_colours: bool,
- white_colour: u8,
- black_colour: u8,
+ board_window: Window,
+ cell_windows: Vec,
+ 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) {
- // 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 = 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
- }
+ 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) {
+ // 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 = 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,
+ 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 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,
+ 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();
- }
+ 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,
+ Player,
+ Log,
}
const ACTION_HEADER: &str = "Enter action in PTN";
const LOG_HEADER: &str = "U to undo a turn | / 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,
+ 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 {} | 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 {
- 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,
- ) -> 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;
- }
+ fn write_header(&self, header: &str) {
+ self.screen.mv(0, 0);
+ self.screen.clrtoeol();
+ self.screen.addstr(format!(
+ "takwrap {} | 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 {
+ 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,
+ ) -> 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;
+ }
}
--
cgit v1.3.1