sashite-epin 1.0.0

Extended Piece Identifier Notation (EPIN): a compact, ASCII-only, no_std token format that extends PIN with a native/derived style marker for abstract strategy board games.
Documentation
//! Transformation and property tests for `sashite-epin`, exercised over the
//! whole closed domain of 624 tokens.

use sashite_epin::{Identifier, Letter, Side, State};
use sashite_pin::Identifier as Pin;

/// All 312 PIN identifiers, in canonical order (letter → side → state → terminal).
fn all_pins() -> Vec<Pin> {
    let mut pins = Vec::with_capacity(312);
    for letter in Letter::ALL {
        for side in [Side::First, Side::Second] {
            for state in [State::Diminished, State::Normal, State::Enhanced] {
                for terminal in [false, true] {
                    pins.push(Pin::new(letter, side, state, terminal));
                }
            }
        }
    }
    pins
}

/// All 624 EPIN identifiers (each PIN identifier, native then derived).
fn all_epins() -> Vec<Identifier> {
    let mut epins = Vec::with_capacity(624);
    for pin in all_pins() {
        epins.push(Identifier::new(pin, false));
        epins.push(Identifier::new(pin, true));
    }
    epins
}

#[test]
fn native_and_derive_set_only_the_flag() {
    for id in all_epins() {
        let n = id.native();
        let d = id.derive();
        assert!(n.is_native());
        assert!(d.is_derived());
        // The PIN core is untouched by either transform.
        assert_eq!(n.pin(), id.pin());
        assert_eq!(d.pin(), id.pin());
    }
}

#[test]
fn native_and_derive_are_idempotent_and_compose() {
    for id in all_epins() {
        assert_eq!(id.native(), id.native().native());
        assert_eq!(id.derive(), id.derive().derive());
        // The last application wins, regardless of the starting flag.
        assert_eq!(id.native().derive(), id.derive());
        assert_eq!(id.derive().native(), id.native());
    }
}

#[test]
fn with_pin_replaces_core_and_keeps_flag() {
    for id in all_epins() {
        let flipped_core = id.pin().flipped();
        let swapped = id.with_pin(flipped_core);
        assert_eq!(swapped.pin(), flipped_core);
        assert_eq!(swapped.is_derived(), id.is_derived());
        // Flipping the core twice through EPIN returns the original identifier.
        assert_eq!(swapped.with_pin(swapped.pin().flipped()), id);
    }
}

#[test]
fn accessors_delegate_to_the_inner_pin() {
    for id in all_epins() {
        let pin = id.pin();
        assert_eq!(id.letter().as_char(), pin.letter().as_char());
        assert_eq!(id.side(), pin.side());
        assert_eq!(id.state(), pin.state());
        assert_eq!(id.is_terminal(), pin.is_terminal());
        assert_eq!(id.is_first(), pin.is_first());
        assert_eq!(id.is_second(), pin.is_second());
        assert_eq!(id.is_native(), !id.is_derived());
    }
}

#[test]
fn encoding_reflects_the_derivation_flag() {
    for id in all_epins() {
        let encoded = id.encode();
        assert_eq!(encoded.as_str().ends_with('\''), id.is_derived());
        // The native form encodes exactly like the underlying PIN core.
        assert_eq!(id.native().encode().as_str(), id.pin().encode().as_str());
    }
}

#[test]
fn native_orders_before_derived_for_the_same_core() {
    for pin in all_pins() {
        assert!(Identifier::new(pin, false) < Identifier::new(pin, true));
    }
}

#[test]
fn sorting_yields_canonical_order() {
    // The generation order is PIN-major, native before derived; the derived
    // `Ord` must reproduce it.
    let canonical = all_epins();
    let mut scrambled = canonical.clone();
    scrambled.reverse();
    scrambled.sort();
    assert_eq!(
        scrambled, canonical,
        "sorting must reproduce the canonical order"
    );
}

#[test]
fn transforms_work_in_const_context() {
    // The PIN core is built at compile time (PIN parsing is const).
    const CORE: Pin = match Pin::parse("+K^") {
        Ok(pin) => pin,
        Err(_) => panic!("\"+K^\" is a valid PIN token"),
    };
    // EPIN construction and transformations are const fns.
    const DERIVED: Identifier = Identifier::new(CORE, true);
    const NATIVE: Identifier = DERIVED.native();
    const FLIPPED: Identifier = DERIVED.with_pin(CORE.flipped());

    // Compare each const result against the equivalent runtime computation, so
    // the assertions are not made on constants (clippy::assertions_on_constants).
    assert_eq!(DERIVED, Identifier::new(Pin::parse("+K^").unwrap(), true));
    assert_eq!(NATIVE, Identifier::new(Pin::parse("+K^").unwrap(), false));
    assert_eq!(FLIPPED, Identifier::new(Pin::parse("+k^").unwrap(), true));
}