Lines
62.01 %
Functions
34.72 %
Branches
100 %
// This file is part of hnefatafl-copenhagen.
//
// hnefatafl-copenhagen is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// hnefatafl-copenhagen 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 Affero General Public License for more details.
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{
fmt,
hash::{Hash, Hasher},
str::FromStr,
};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::{
board::BoardSize,
role::Role,
time::{TimeLeft, TimeSettings},
pub const BOARD_LETTERS: &str = "ABCDEFGHIJKLM";
pub const EXIT_SQUARES_11X11: [Vertex; 4] = [
Vertex {
size: BoardSize::_11,
x: 0,
y: 0,
},
x: 10,
y: 10,
];
const THRONE_11X11: Vertex = Vertex {
x: 5,
y: 5,
const RESTRICTED_SQUARES_11X11: [Vertex; 5] = [
THRONE_11X11,
pub const EXIT_SQUARES_13X13: [Vertex; 4] = [
size: BoardSize::_13,
x: 12,
y: 12,
const THRONE_13X13: Vertex = Vertex {
x: 6,
y: 6,
const RESTRICTED_SQUARES_13X13: [Vertex; 5] = [
THRONE_13X13,
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialOrd, Serialize)]
pub struct PlayRecordTimed {
pub play: Option<Plae>,
pub attacker_time: TimeLeft,
pub defender_time: TimeLeft,
}
impl Hash for PlayRecordTimed {
fn hash<H: Hasher>(&self, state: &mut H) {
self.play.hash(state);
impl PartialEq for PlayRecordTimed {
fn eq(&self, other: &Self) -> bool {
self.play == other.play
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum Plae {
Play(Play),
AttackerResigns,
DefenderResigns,
impl fmt::Display for Plae {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Play(play) => write!(f, "play {} {} {}", play.role, play.from, play.to),
Self::AttackerResigns => write!(f, "play attacker resigns _"),
Self::DefenderResigns => write!(f, "play defender resigns _"),
impl Plae {
/// # Errors
///
/// If you try to convert an illegal character or you don't get vertex-vertex.
pub fn from_str_(play: &str, role: &Role) -> anyhow::Result<Self> {
let Some((from, to)) = play.split_once('-') else {
return Err(anyhow::Error::msg("expected: vertex-vertex"));
Ok(Self::Play(Play {
role: *role,
from: Vertex::from_str(from)?,
to: Vertex::from_str(to)?,
}))
impl TryFrom<Vec<&str>> for Plae {
type Error = anyhow::Error;
fn try_from(args: Vec<&str>) -> Result<Self, Self::Error> {
let error_str = "expected: 'play ROLE FROM TO' or 'play ROLE resign'";
if args.len() < 3 {
return Err(anyhow::Error::msg(error_str));
let role = Role::from_str(args[1])?;
if args[2] == "resigns" {
if role == Role::Defender {
return Ok(Self::DefenderResigns);
return Ok(Self::AttackerResigns);
if args.len() < 4 {
role: Role::from_str(args[1])?,
from: Vertex::from_str(args[2])?,
to: Vertex::from_str(args[3])?,
pub enum Plays {
PlayRecordsTimed(Vec<PlayRecordTimed>),
PlayRecords(Vec<Option<Plae>>),
impl Plays {
#[must_use]
pub fn is_empty(&self) -> bool {
Plays::PlayRecordsTimed(plays) => plays.is_empty(),
Plays::PlayRecords(plays) => plays.is_empty(),
pub fn len(&self) -> usize {
Plays::PlayRecordsTimed(plays) => plays.len(),
Plays::PlayRecords(plays) => plays.len(),
pub fn new(time_settings: &TimeSettings) -> Self {
match time_settings {
TimeSettings::Timed(_) => Plays::PlayRecordsTimed(Vec::new()),
TimeSettings::UnTimed => Plays::PlayRecords(Vec::new()),
pub fn time_left(&self, role: Role, index: usize) -> String {
Plays::PlayRecordsTimed(plays) => match role {
Role::Attacker => {
if let Some(play) = plays.get(index) {
play.attacker_time.to_string()
} else {
"-".to_string()
Role::Defender => {
play.defender_time.to_string()
Role::Roleless => unreachable!(),
Plays::PlayRecords(_) => "-".to_string(),
impl Default for Plays {
fn default() -> Self {
Plays::PlayRecordsTimed(Vec::new())
impl fmt::Display for Plays {
Plays::PlayRecordsTimed(plays) => {
for play in plays {
if let Some(play) = &play.play {
write!(f, "{play}, ")?;
Plays::PlayRecords(plays) => {
if let Some(play) = &play {
Ok(())
pub struct Play {
pub role: Role,
pub from: Vertex,
pub to: Vertex,
impl fmt::Display for Play {
write!(f, "{:?} from {} to {}", self.role, self.from, self.to)
#[derive(Debug, Default)]
pub struct Captures(pub Vec<Vertex>);
impl fmt::Display for Captures {
for vertex in &self.0 {
write!(f, "{vertex} ")?;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Vertex {
pub size: BoardSize,
pub x: usize,
pub y: usize,
impl fmt::Display for Vertex {
let letters = match self.size {
BoardSize::_11 => &BOARD_LETTERS.to_lowercase(),
BoardSize::_13 => BOARD_LETTERS,
let board_size: usize = self.size.into();
write!(
f,
"{}{}",
letters.chars().collect::<Vec<_>>()[self.x],
board_size - self.y
)
impl FromStr for Vertex {
type Err = anyhow::Error;
fn from_str(vertex: &str) -> anyhow::Result<Self> {
let mut chars = vertex.chars();
if let Some(mut ch) = chars.next() {
let size = if ch.is_lowercase() { 11 } else { 13 };
ch = ch.to_ascii_uppercase();
let x = BOARD_LETTERS[..size]
.find(ch)
.context("play: the first letter is not a legal char")?;
let mut y = chars.as_str().parse()?;
if y > 0 && y <= size {
y = size - y;
return Ok(Self {
size: size.try_into()?,
x,
y,
});
Err(anyhow::Error::msg("play: invalid coordinate"))
impl Vertex {
pub fn up(&self) -> Option<Vertex> {
if self.y > 0 {
Some(Vertex {
size: self.size,
x: self.x,
y: self.y - 1,
})
None
pub fn left(&self) -> Option<Vertex> {
if self.x > 0 {
x: self.x - 1,
y: self.y,
pub fn down(&self) -> Option<Vertex> {
if self.y < board_size - 1 {
y: self.y + 1,
#[inline]
pub fn on_exit_square(&self) -> bool {
match self.size {
BoardSize::_11 => EXIT_SQUARES_11X11.contains(self),
BoardSize::_13 => EXIT_SQUARES_13X13.contains(self),
pub fn on_throne(&self) -> bool {
BoardSize::_11 => THRONE_11X11 == *self,
BoardSize::_13 => THRONE_13X13 == *self,
pub fn on_restricted_square(&self) -> bool {
match &self.size {
BoardSize::_11 => RESTRICTED_SQUARES_11X11.contains(self),
BoardSize::_13 => RESTRICTED_SQUARES_13X13.contains(self),
pub fn right(&self) -> Option<Vertex> {
if self.x < board_size - 1 {
x: self.x + 1,
pub fn touches_wall(&self) -> bool {
self.x == 0 || self.x == board_size - 1 || self.y == 0 || self.y == board_size - 1
impl From<&Vertex> for usize {
fn from(vertex: &Vertex) -> Self {
let board_size: usize = vertex.size.into();
vertex.y * board_size + vertex.x