Skip to content

Commit

Permalink
Merge pull request #8 from scottbedard/sequence
Browse files Browse the repository at this point in the history
Implement notation sequences
  • Loading branch information
scottbedard authored Aug 30, 2024
2 parents 2440926 + 5200dbc commit f4d2935
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 9 deletions.
28 changes: 19 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A Rust / TypeScript library for [Gliński's hexagonal chess](https://en.wikipedi

## Basic usage

Execute `hexchess` to open the following command line interface. More documentation to come.
Execute `hexchess` to open the following command line interface.

```
Usage: hexchess <COMMAND>
Expand All @@ -40,7 +40,7 @@ A collection of wasm bindings available via `@bedard/hexchess`, listed below are
#### `applyNotation`

Create a new `Hexchess` object after applying a piece of `Notation`.
Create a new `Hexchess` object and apply a single `Notation`.

```ts
import { applyNotation } from '@bedard/hexchess'
Expand All @@ -50,6 +50,18 @@ applyNotation(hexchess, notation)
// { board: { ... }, enPassant, turn, fullmove, halfmove }
```

#### `applySequence`

Create a new `Hexchess` object and apply a whitespace-separated sequence of moves. An error is thrown if a piece of notation is not valid or a move is illegal.

```ts
import { applySequence } from '@bedard/hexchess'

applySequence(hexchess, 'g4g5 e7e6')

// { board: { ... }, enPassant, turn, fullmove, halfmove }
```

#### `createHexchess`

Create an empty `Hexchess` object.
Expand All @@ -76,7 +88,7 @@ createHexchessInitial()

#### `findKing`

Find a player's king
Find a player's king.

```ts
import { findKing } from '@bedard/hexchess'
Expand All @@ -98,19 +110,17 @@ getColor('?') // null

#### `getPositionColor`

Get color of a piece by board position.
Get color of a piece by board position. If no piece is present, `null` will be returned.

```ts
import { getPositionColor } from '@bedard/hexchess'

getPositionColor(hexchess, 'f5') // 'w'
getPositionColor(hexchess, 'f6') // null
getPositionColor(hexchess, 'f7') // 'b'
```

#### `getTargets`

Find all legal moves from a position and return the resulting array of `Notation` objects.
Find all legal moves from a position and return an array of `Notation` objects.

```ts
import { getTargets } from '@bedard/hexchess'
Expand All @@ -122,7 +132,7 @@ targets(hexchess, 'g4')

#### `isCheckmate`

Test if the board is in checkmate
Test if the board is in checkmate.

```ts
import { isCheckmate } from '@bedard/hexchess'
Expand All @@ -132,7 +142,7 @@ isCheckmate(hexchess) // true / false

#### `isThreatened`

Test if a position is threatened
Test if a position is threatened.

```ts
import { isThreatened } from '@bedard/hexchess'
Expand Down
90 changes: 90 additions & 0 deletions src/game/hexchess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub struct Hexchess {

/// Create hexchess from fen
impl Hexchess {
/// Apply a legal move to the game state
pub fn apply(&mut self, notation: Notation) -> Result<(), Failure> {
let piece = match self.board.get(notation.from) {
Some(val) => val,
Expand All @@ -38,9 +39,15 @@ impl Hexchess {
return Err(Failure::OutOfTurn);
}

// verify the piece can move to the target position
if !self.targets(notation.from).contains(&notation) {
return Err(Failure::IllegalMove);
}

self.apply_unsafe(notation)
}

/// Apply an arbitrary move to the game state, disregarding turn or legality
pub fn apply_unsafe(&mut self, notation: Notation) -> Result<(), Failure> {
let piece = match self.board.get(notation.from) {
Some(val) => val,
Expand Down Expand Up @@ -126,6 +133,36 @@ impl Hexchess {
Ok(())
}

/// Apply a sequence of moves
pub fn apply_sequence(&mut self, sequence: &str) -> Result<(), String> {
let mut clone = self.clone();
let mut i: u16 = 0;

for part in sequence.split_whitespace() {
let notation = match Notation::from(part) {
Ok(notation) => notation,
Err(_) => {
return Err(format!("Invalid notation at index {}: {}", i, part));
},
};

if clone.apply(notation).is_err() {
return Err(format!("Illegal move at index {}: {}", i, part));
}

i += 1;
}

self.board = clone.board;
self.en_passant = clone.en_passant;
self.fullmove = clone.fullmove;
self.halfmove = clone.halfmove;
self.turn = clone.turn;

Ok(())
}

/// Create hexchess from string
pub fn from(value: &str) -> Result<Self, Failure> {
let mut parts = value.split_whitespace();

Expand Down Expand Up @@ -184,13 +221,15 @@ impl Hexchess {
})
}

/// Get the color of a position
pub fn color(&self, position: Position) -> Option<Color> {
match self.board.get(position) {
None => None,
Some(piece) => Some(piece.color()),
}
}

/// Find the king of a given color
pub fn find_king(&self, color: Color) -> Option<Position> {
let king = match color {
Color::White => Some(Piece::WhiteKing),
Expand All @@ -206,6 +245,7 @@ impl Hexchess {
return None
}

/// Create hexchess with initial board state
pub fn initial() -> Self {
Hexchess {
board: Board::initial(),
Expand All @@ -216,6 +256,7 @@ impl Hexchess {
}
}

/// Test if the board is currently in checkmate
pub fn is_checkmate(&self) -> bool {
let king_position = match self.find_king(self.turn) {
Some(p) => p,
Expand Down Expand Up @@ -244,6 +285,7 @@ impl Hexchess {
false
}

/// Test if a position is threatened by an enemy piece
pub fn is_threatened(&self, position: Position) -> bool {
let piece = match self.board.get(position) {
Some(val) => val,
Expand Down Expand Up @@ -275,6 +317,7 @@ impl Hexchess {
false
}

/// Create an empty hexchess
pub fn new() -> Self {
Hexchess {
board: Board::new(),
Expand All @@ -285,6 +328,7 @@ impl Hexchess {
}
}

/// Get the legal targets from a position
pub fn targets(&self, position: Position) -> Vec<Notation> {
let color = match self.color(position) {
Some(val) => val,
Expand All @@ -306,6 +350,7 @@ impl Hexchess {
.collect()
}

/// Get all targets from a position, including potential self-checks
pub fn targets_unsafe(&self, position: Position) -> Vec<Notation> {
let piece = match self.board.get(position) {
Some(val) => val,
Expand All @@ -328,6 +373,7 @@ impl Hexchess {
}
}

/// Stringify a hexchess to JSON
pub fn to_json(&self) -> String {
json!(self).to_string()
}
Expand Down Expand Up @@ -462,6 +508,15 @@ mod tests {
assert_eq!(Err(Failure::IllegalMove), result);
}

#[test]
fn test_apply_illegal_move() {
let mut hexchess = Hexchess::new();

let result = hexchess.apply(Notation::from("f5a1").unwrap());

assert_eq!(Err(Failure::IllegalMove), result);
}

#[test]
fn test_apply_sets_black_en_passant() {
let mut b = Hexchess::from("1/3/5/7/p8/11/11/11/11/11/11 b - 0 1").unwrap();
Expand Down Expand Up @@ -871,4 +926,39 @@ mod tests {
assert_eq!(None, blank.find_king(Color::White));
assert_eq!(None, blank.find_king(Color::Black));
}

#[test]
fn test_applying_a_sequence_of_moves() {
let mut hexchess = Hexchess::initial();
let _ = hexchess.apply_sequence("g4g6 f7g6 f5f7 g6f6");

assert_eq!(hexchess.to_string(), "b/qbk/n1b1n/r5r/pppp1pppp/5p5/11/4P6/3P1B1P3/2P2B2P2/1PRNQBKNRP1 w - 0 3");
}

#[test]
fn test_apply_sequence_with_invalid_move() {
let mut hexchess = Hexchess::initial();
let result = hexchess.apply_sequence("g4g5 whoops");

assert_eq!(hexchess.to_string(), INITIAL_HEXCHESS); // <- the board has not changed
assert_eq!(Err(String::from("Invalid notation at index 1: whoops")), result);
}

#[test]
fn test_apply_sequence_with_illegal_move() {
let mut hexchess = Hexchess::initial();
let result = hexchess.apply_sequence("g4g5 b7a1"); // <- b7 is a black pawn, it cannot move to a1

assert_eq!(hexchess.to_string(), INITIAL_HEXCHESS); // <- the board has not changed
assert_eq!(Err(String::from("Illegal move at index 1: b7a1")), result);
}

#[test]
fn test_apply_sequence_that_attempts_a_self_check() {
let mut hexchess = Hexchess::initial();
let result = hexchess.apply_sequence("f2d4 g10g9");

assert_eq!(hexchess.to_string(), INITIAL_HEXCHESS);
assert_eq!(Err(String::from("Illegal move at index 1: g10g9")), result);
}
}
12 changes: 12 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ pub fn apply_notation(hexchess: Hexchess, notation: Notation) -> Hexchess {
output
}

/// Execute a sequence of moves
#[wasm_bindgen(js_name = applySequence)]
pub fn apply_sequence(hexchess: Hexchess, sequence: &str) -> JsValue {
let mut output = hexchess.clone();
let result = output.apply_sequence(sequence);

match result {
Ok(_) => JsValue::from_serde(&output).unwrap(),
Err(message) => panic!("{}", message),
}
}

/// Create empty hexchess object
#[wasm_bindgen(js_name = createHexchess)]
pub fn create_hexchess() -> Hexchess {
Expand Down
15 changes: 15 additions & 0 deletions vitest/apply-sequence.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
applySequence,
createHexchessInitial,
stringifyHexchess,
} from '@bedard/hexchess'

import { describe, expect, it } from 'vitest'

describe('applySequence', () => {
it('executes a sequence of moves', () => {
const hexchess = applySequence(createHexchessInitial(), 'g4g6 f7g6 f5f7 g6f6')

expect(stringifyHexchess(hexchess)).toBe('b/qbk/n1b1n/r5r/pppp1pppp/5p5/11/4P6/3P1B1P3/2P2B2P2/1PRNQBKNRP1 w - 0 3')
})
})

0 comments on commit f4d2935

Please sign in to comment.