use std::collections::HashSet;
use crate::rule::{IllegalMove, Rules};
use crate::{Error, Result};
#[derive(Clone, Copy, PartialEq, Debug, Hash)]
#[repr(u8)]
pub enum Stone {
Empty,
Black,
White,
}
impl std::ops::Not for Stone {
type Output = Self;
fn not(self) -> Self::Output {
match self {
Stone::Black => Stone::White,
Stone::White => Stone::Black,
Stone::Empty => Stone::Empty,
}
}
}
#[derive(Clone, PartialEq, Hash)]
pub struct Board {
stones: Vec<Stone>,
size: (usize, usize),
hashes: Vec<u64>,
}
impl Board {
pub fn empty(width: usize, height: usize) -> Self {
Self {
stones: vec![Stone::Empty; width * height],
size: (width, height),
hashes: Vec::new(),
}
}
fn index(&self, x: usize, y: usize) -> Result<usize> {
if x >= self.size.0 {
return Err(Error::CoordinatesOutOfBounds);
}
if y >= self.size.1 {
return Err(Error::CoordinatesOutOfBounds);
}
return Ok(y * self.size.0 + x);
}
pub fn get(&self, x: usize, y: usize) -> Result<Stone> {
let i = self.index(x, y)?;
return Ok(self.stones[i]);
}
fn set(&mut self, x: usize, y: usize, s: Stone) -> Result<()> {
let i = self.index(x, y)?;
self.stones[i] = s;
Ok(())
}
pub fn place(&mut self, x: usize, y: usize, s: Stone, rules: &Rules) -> Result<PlayResponse> {
let mut new = self.clone();
let mut response = PlayResponse::default();
new.set(x, y, s)?;
let group = new.get_group(x, y)?;
let mut enemy_groups: Vec<Group> = Vec::new();
let mut categorized: HashSet<(usize, usize)> = HashSet::new();
for s in group.enemy_neighbors {
if !categorized.contains(&s) {
enemy_groups.push(new.get_group(s.0, s.1)?);
}
categorized.insert(s);
}
for g in enemy_groups {
if g.liberties.is_empty() {
new.kill_group(&g)?;
if g.color == Stone::Black {
response.black_captures += g.points.len() as u16;
}
if g.color == Stone::White {
response.white_captures += g.points.len() as u16;
}
}
}
let group = new.get_group(x, y)?;
if !rules.suicide_allowed && group.liberties.is_empty() {
return Err(Error::IllegalMove(IllegalMove::SuicidalMove));
}
let hash = fxhash::hash64(&new.stones);
new.hashes.push(hash);
*self = new;
Ok(response)
}
pub fn play(&mut self, x: usize, y: usize, s: Stone, rules: &Rules) -> Result<PlayResponse> {
let mut new = self.clone();
let mut response = PlayResponse::default();
if self.get(x, y)? != Stone::Empty {
return Err(Error::IllegalMove(IllegalMove::NonEmptySpace));
}
new.set(x, y, s)?;
let group = new.get_group(x, y)?;
let mut enemy_groups: Vec<Group> = Vec::new();
let mut categorized: HashSet<(usize, usize)> = HashSet::new();
for s in group.enemy_neighbors {
if !categorized.contains(&s) {
enemy_groups.push(new.get_group(s.0, s.1)?);
}
categorized.insert(s);
}
for g in enemy_groups {
if g.liberties.is_empty() {
new.kill_group(&g)?;
if g.color == Stone::Black {
response.black_captures += g.points.len() as u16;
}
if g.color == Stone::White {
response.white_captures += g.points.len() as u16;
}
}
}
let group = new.get_group(x, y)?;
if !rules.suicide_allowed && group.liberties.is_empty() {
return Err(Error::IllegalMove(IllegalMove::SuicidalMove));
}
let hash = fxhash::hash64(&new.stones);
if Some(&hash) == new.hashes.iter().rev().nth(1) {
return Err(Error::IllegalMove(IllegalMove::Ko));
}
if new.hashes.contains(&hash) && rules.superko {
return Err(Error::IllegalMove(IllegalMove::SuperKo));
}
new.hashes.push(hash);
*self = new;
Ok(response)
}
pub fn size(&self) -> (usize, usize) {
self.size
}
fn kill_group(&mut self, g: &Group) -> Result<()> {
for s in &g.points {
self.set(s.0, s.1, Stone::Empty)?;
}
Ok(())
}
pub fn get_group(&self, x: usize, y: usize) -> Result<Group> {
let mut group = Group {
color: self.get(x, y)?,
points: HashSet::new(),
liberties: HashSet::new(),
enemy_neighbors: HashSet::new(),
};
group.points.insert((x, y));
self.build_group(&mut group, (x, y));
return Ok(group);
}
fn try_group_point(&self, x: usize, y: usize, group: &mut Group) {
if group.categorized((x, y)) {
return;
}
let stone = match self.get(x, y) {
Ok(s) => s,
Err(_) => return,
};
if stone == group.color {
group.points.insert((x, y));
self.build_group(group, (x, y))
} else if stone == Stone::Empty {
group.liberties.insert((x, y));
} else {
group.enemy_neighbors.insert((x, y));
}
}
fn build_group(&self, group: &mut Group, p: (usize, usize)) {
let left_edge = p.0 == 0;
let top_edge = p.1 == 0;
if !left_edge {
self.try_group_point(p.0 - 1, p.1, group);
}
if !top_edge {
self.try_group_point(p.0, p.1 - 1, group);
}
self.try_group_point(p.0 + 1, p.1, group);
self.try_group_point(p.0, p.1 + 1, group);
}
pub fn star_points(&self) -> Vec<(usize, usize)> {
let mut points = Vec::new();
let (w, h) = self.size;
if w % 2 == 1 && h % 2 == 1 {
points.push((w / 2, h / 2));
}
if w < 9 || h < 9 {
return points;
}
if w < 13 || h < 13 {
points.push((2, 2));
points.push((2, h - 3));
points.push((w - 3, 2));
points.push((w - 3, h - 3));
return points;
}
if w > 13 {
if h % 2 == 1 {
points.push((3, h / 2));
points.push((w - 4, h / 2));
}
if w % 2 == 1 {
points.push((w / 2, 3));
points.push((w / 2, h - 4));
}
}
points.push((3, 3));
points.push((3, h - 4));
points.push((w - 4, 3));
points.push((w - 4, h - 4));
return points;
}
}
impl Default for Board {
fn default() -> Self {
Self::empty(19, 19)
}
}
pub struct Group {
pub color: Stone,
pub points: HashSet<(usize, usize)>,
pub liberties: HashSet<(usize, usize)>,
pub enemy_neighbors: HashSet<(usize, usize)>,
}
impl Group {
pub fn categorized(&self, p: (usize, usize)) -> bool {
self.points.contains(&p) || self.liberties.contains(&p) || self.enemy_neighbors.contains(&p)
}
}
#[derive(Clone, Copy, PartialEq, Default, Debug)]
pub struct PlayResponse {
pub black_captures: u16,
pub white_captures: u16,
}
#[cfg(test)]
mod board_tests {
use super::*;
#[test]
fn correct_index() {
let board = Board::empty(9, 9);
assert_eq!(board.index(0, 0).unwrap(), 0);
assert_eq!(board.index(4, 4).unwrap(), 40);
assert_eq!(board.index(8, 8).unwrap(), 80);
}
#[test]
fn wrong_index() {
let board = Board::empty(9, 9);
assert_eq!(board.index(9, 0), Err(Error::CoordinatesOutOfBounds));
assert_eq!(board.index(9, 9), Err(Error::CoordinatesOutOfBounds));
assert_eq!(board.index(0, 9), Err(Error::CoordinatesOutOfBounds));
}
#[test]
fn non_empty_space() {
let mut board = Board::empty(9, 9);
let rules = Rules::JAPANESE;
board
.play(0, 0, Stone::White, &rules)
.expect("failed to play!");
assert_eq!(
board.play(0, 0, Stone::White, &rules),
Err(Error::IllegalMove(IllegalMove::NonEmptySpace))
);
}
}
#[cfg(test)]
mod group_tests {
use super::*;
#[test]
fn center_group() {
let mut board = Board::empty(9, 9);
let mut points_in_group: HashSet<(usize, usize)> = HashSet::new();
points_in_group.insert((3, 2));
points_in_group.insert((4, 2));
points_in_group.insert((4, 3));
points_in_group.insert((4, 4));
points_in_group.insert((5, 4));
points_in_group.insert((5, 5));
let rules = Rules::JAPANESE;
for p in &points_in_group {
board
.play(p.0, p.1, Stone::Black, &rules)
.expect("Failed to play");
}
let group = board.get_group(3, 2).expect("Failed to create group");
assert_eq!(group.points, points_in_group);
}
#[test]
fn left_group() {
let mut board = Board::empty(9, 9);
let mut points_in_group: HashSet<(usize, usize)> = HashSet::new();
points_in_group.insert((0, 4));
points_in_group.insert((0, 5));
points_in_group.insert((1, 4));
points_in_group.insert((1, 5));
points_in_group.insert((2, 3));
points_in_group.insert((2, 4));
points_in_group.insert((3, 3));
points_in_group.insert((3, 4));
let rules = Rules::JAPANESE;
for p in &points_in_group {
board
.play(p.0, p.1, Stone::Black, &rules)
.expect("Failed to play");
}
let group = board.get_group(0, 4).expect("Failed to create group");
assert_eq!(group.points, points_in_group);
}
#[test]
fn right_group() {
let mut board = Board::empty(9, 9);
let mut points_in_group: HashSet<(usize, usize)> = HashSet::new();
points_in_group.insert((8, 4));
points_in_group.insert((8, 5));
points_in_group.insert((8, 6));
points_in_group.insert((7, 3));
points_in_group.insert((7, 4));
points_in_group.insert((7, 6));
points_in_group.insert((6, 4));
points_in_group.insert((6, 6));
points_in_group.insert((5, 6));
points_in_group.insert((4, 6));
points_in_group.insert((4, 5));
points_in_group.insert((3, 6));
points_in_group.insert((3, 5));
let rules = Rules::JAPANESE;
for p in &points_in_group {
board
.play(p.0, p.1, Stone::Black, &rules)
.expect("Failed to play");
}
let group = board.get_group(8, 4).expect("Failed to create group");
assert_eq!(group.points, points_in_group);
}
#[test]
fn round_group() {
let mut board = Board::empty(9, 9);
let mut points_in_group: HashSet<(usize, usize)> = HashSet::new();
points_in_group.insert((0, 0));
points_in_group.insert((0, 1));
points_in_group.insert((0, 2));
points_in_group.insert((0, 3));
points_in_group.insert((0, 4));
points_in_group.insert((0, 5));
points_in_group.insert((0, 6));
points_in_group.insert((0, 7));
points_in_group.insert((0, 8));
points_in_group.insert((1, 0));
points_in_group.insert((2, 0));
points_in_group.insert((3, 0));
points_in_group.insert((4, 0));
points_in_group.insert((5, 0));
points_in_group.insert((6, 0));
points_in_group.insert((7, 0));
points_in_group.insert((8, 0));
points_in_group.insert((8, 1));
points_in_group.insert((8, 2));
points_in_group.insert((8, 3));
points_in_group.insert((8, 4));
points_in_group.insert((8, 5));
points_in_group.insert((8, 6));
points_in_group.insert((8, 7));
points_in_group.insert((8, 8));
points_in_group.insert((1, 8));
points_in_group.insert((2, 8));
points_in_group.insert((3, 8));
points_in_group.insert((4, 8));
points_in_group.insert((5, 8));
points_in_group.insert((6, 8));
points_in_group.insert((6, 8));
points_in_group.insert((7, 8));
let rules = Rules::JAPANESE;
for p in &points_in_group {
board
.play(p.0, p.1, Stone::Black, &rules)
.expect("Failed to play");
}
let group = board.get_group(0, 0).expect("Failed to create group");
assert_eq!(group.points, points_in_group);
}
#[test]
fn group_neighbors() {
let mut board = Board::empty(9, 9);
let mut black: HashSet<(usize, usize)> = HashSet::new();
let mut white: HashSet<(usize, usize)> = HashSet::new();
black.insert((3, 3));
black.insert((3, 4));
black.insert((3, 5));
white.insert((2, 3));
white.insert((2, 4));
white.insert((2, 5));
let rules = Rules::JAPANESE;
for p in &black {
board
.play(p.0, p.1, Stone::Black, &rules)
.expect("Failed to play");
}
for p in &white {
board
.play(p.0, p.1, Stone::White, &rules)
.expect("Failed to play");
}
let black_group = board.get_group(3, 3).expect("Failed to create group");
let white_group = board.get_group(2, 3).expect("Failed to create group");
assert_eq!(black, black_group.points);
assert_eq!(white, white_group.points);
assert_eq!(black, white_group.enemy_neighbors);
assert_eq!(white, black_group.enemy_neighbors);
}
#[test]
fn single_stone_group() {
let mut board = Board::empty(9, 9);
board
.play(5, 5, Stone::Black, &Rules::JAPANESE)
.expect("failed to play");
let mut intended = HashSet::new();
intended.insert((5, 5));
let group = board.get_group(5, 5).unwrap();
assert_eq!(group.points, intended);
}
}
#[cfg(test)]
mod capturing_tests {
use super::*;
#[test]
fn kill_group_center() -> Result<()> {
let mut board = Board::empty(9, 9);
let rules = Rules::JAPANESE;
board.play(3, 4, Stone::White, &rules)?;
board.play(4, 4, Stone::White, &rules)?;
board.play(4, 3, Stone::White, &rules)?;
board.play(5, 3, Stone::White, &rules)?;
board.play(2, 4, Stone::Black, &rules)?;
board.play(3, 3, Stone::Black, &rules)?;
board.play(3, 5, Stone::Black, &rules)?;
board.play(4, 2, Stone::Black, &rules)?;
board.play(4, 5, Stone::Black, &rules)?;
board.play(5, 2, Stone::Black, &rules)?;
board.play(5, 4, Stone::Black, &rules)?;
let response = board.play(6, 3, Stone::Black, &rules)?;
assert_eq!(board.get(3, 4)?, Stone::Empty);
assert_eq!(board.get(4, 4)?, Stone::Empty);
assert_eq!(board.get(4, 3)?, Stone::Empty);
assert_eq!(board.get(5, 3)?, Stone::Empty);
assert_eq!(
response,
PlayResponse {
black_captures: 0,
white_captures: 4,
}
);
Ok(())
}
#[test]
fn single_ko() -> Result<()> {
let mut board = Board::empty(9, 9);
let rules = Rules::JAPANESE;
board.play(4, 2, Stone::Black, &rules)?;
board.play(3, 3, Stone::Black, &rules)?;
board.play(5, 3, Stone::Black, &rules)?;
board.play(3, 4, Stone::White, &rules)?;
board.play(4, 5, Stone::White, &rules)?;
board.play(5, 4, Stone::White, &rules)?;
board.play(4, 3, Stone::White, &rules)?;
board.play(4, 4, Stone::Black, &rules)?;
assert_eq!(
board.play(4, 3, Stone::White, &rules),
Err(Error::IllegalMove(IllegalMove::Ko))
);
Ok(())
}
}