use crate::natural_state::NaturalState;
use crate::session::SessionParams;
use sashite_sanki_engine::domain::side::Side;
#[must_use]
pub fn accepts_standing_offer(
params: &SessionParams,
natural: &NaturalState<'_>,
invoker: Side,
) -> bool {
let Some(last) = natural.chain.last() else {
return false;
};
if !last.ply.draw {
return false;
}
params
.side_of(last.ply.signer)
.is_some_and(|offerer| invoker == offerer.flip())
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
use super::accepts_standing_offer;
use crate::event::{EventId, Ply, PublicKey};
use crate::natural_state::{CanonicalPly, ChainEnd, NaturalState};
use crate::session::{Seats, SessionParams};
use sashite_sanki_engine::domain::side::Side;
use sashite_sanki_engine::domain::time::{Duration, Timestamp};
use sashite_sanki_engine::domain::time_control::{Period, TimeControl};
use sashite_sanki_engine::position::Position;
const FIRST: u8 = 10;
const SECOND: u8 = 20;
const SESSION: u8 = 50;
fn pk(byte: u8) -> PublicKey {
PublicKey::from_bytes([byte; 32])
}
fn eid(byte: u8) -> EventId {
EventId::from_bytes([byte; 32])
}
fn ts(secs: i64) -> Timestamp {
Timestamp::from_unix(secs)
}
fn ply(id: u8, signer: u8, step: u32, draw: bool) -> Ply {
Ply::new(
eid(id),
pk(signer),
eid(SESSION),
step,
draw,
String::new(),
ts(0),
)
}
fn params() -> SessionParams {
let period = Period::new(Duration::from_secs(600), None, None).expect("valid period");
SessionParams::new(
eid(SESSION),
Some(pk(99)),
Seats::new(pk(FIRST), pk(SECOND)).expect("distinct"),
TimeControl::new(period, Vec::new()),
Position::parse("4k^3/8/8/8/8/8/8/4K^3 / W/w").expect("valid FEEN"),
ts(0),
)
.expect("first to move")
}
const fn side(who: u8) -> Side {
if who == FIRST {
Side::First
} else {
Side::Second
}
}
#[test]
fn agreement_when_opponent_accepts_the_draw() {
let p1 = ply(1, FIRST, 1, true);
let natural = NaturalState {
chain: vec![CanonicalPly {
ply: &p1,
at: ts(100),
}],
cutoff: ts(1000),
end: ChainEnd::Ongoing(Box::new(params().initial_state())),
};
assert!(accepts_standing_offer(¶ms(), &natural, side(SECOND)));
}
#[test]
fn no_acceptance_without_a_draw_flag() {
let p1 = ply(1, FIRST, 1, false);
let natural = NaturalState {
chain: vec![CanonicalPly {
ply: &p1,
at: ts(100),
}],
cutoff: ts(1000),
end: ChainEnd::Ongoing(Box::new(params().initial_state())),
};
assert!(!accepts_standing_offer(¶ms(), &natural, side(SECOND)));
}
#[test]
fn offer_extended_past_by_play_is_declined() {
let p1 = ply(1, FIRST, 1, true);
let p2 = ply(2, SECOND, 1, false);
let natural = NaturalState {
chain: vec![
CanonicalPly {
ply: &p1,
at: ts(100),
},
CanonicalPly {
ply: &p2,
at: ts(200),
},
],
cutoff: ts(1000),
end: ChainEnd::Ongoing(Box::new(params().initial_state())),
};
assert!(!accepts_standing_offer(¶ms(), &natural, side(FIRST)));
}
#[test]
fn offerer_cannot_accept_their_own_offer() {
let p1 = ply(1, FIRST, 1, true);
let natural = NaturalState {
chain: vec![CanonicalPly {
ply: &p1,
at: ts(100),
}],
cutoff: ts(1000),
end: ChainEnd::Ongoing(Box::new(params().initial_state())),
};
assert!(!accepts_standing_offer(¶ms(), &natural, side(FIRST)));
}
#[test]
fn empty_chain_has_no_offer() {
let natural = NaturalState {
chain: Vec::new(),
cutoff: ts(1000),
end: ChainEnd::Ongoing(Box::new(params().initial_state())),
};
assert!(!accepts_standing_offer(¶ms(), &natural, side(SECOND)));
}
const TIMESTAMPER: u8 = 99;
const CONCLUSION: u8 = 170;
const ROOK_KING: &str = "4k^3/8/8/8/8/8/8/R3K^3 / W/w";
const RA1A4: &str = "[\"a1\",\"a4\",null]"; const RA1A5: &str = "[\"a1\",\"a5\",null]"; const KE8E7: &str = "[\"e8\",\"e7\",null]"; const KE8E6: &str = "[\"e8\",\"e6\",null]"; const RA4A5: &str = "[\"a4\",\"a5\",null]";
fn played(id: u8, signer: u8, step: u32, draw: bool, content: &str) -> Ply {
Ply::new(
eid(id),
pk(signer),
eid(SESSION),
step,
draw,
content.to_owned(),
ts(0),
)
}
fn att(id: u8, attests: u8, at: i64) -> crate::event::Attestation {
crate::event::Attestation::new(eid(id), pk(TIMESTAMPER), eid(attests), ts(at))
}
fn rook_params() -> SessionParams {
let period = Period::new(Duration::from_secs(100_000), None, None).expect("valid period");
SessionParams::new(
eid(SESSION),
Some(pk(TIMESTAMPER)),
Seats::new(pk(FIRST), pk(SECOND)).expect("distinct"),
TimeControl::new(period, Vec::new()),
Position::parse(ROOK_KING).expect("valid FEEN"),
ts(0),
)
.expect("first to move")
}
fn acceptances(
params: &SessionParams,
plies: &[Ply],
attestations: &[crate::event::Attestation],
) -> (bool, bool) {
let cutoff = attestations
.iter()
.find(|a| a.attests == eid(CONCLUSION))
.map(|a| a.created_at)
.expect("the fixture attests the conclusion");
let natural = crate::natural_state::natural_state(params, plies, attestations, cutoff);
(
accepts_standing_offer(params, &natural, side(FIRST)),
accepts_standing_offer(params, &natural, side(SECOND)),
)
}
#[test]
fn an_anterior_premove_may_carry_the_standing_offer() {
let p = rook_params();
let plies = [
played(1, FIRST, 1, false, RA1A4),
played(2, SECOND, 1, true, KE8E7),
];
let atts = [
att(101, 1, 100),
att(102, 2, 50),
att(171, CONCLUSION, 1000),
];
assert_eq!(acceptances(&p, &plies, &atts), (true, false));
}
#[test]
fn only_the_tail_offer_stands_when_both_half_moves_offer() {
let p = rook_params();
let plies = [
played(1, FIRST, 1, true, RA1A4),
played(2, SECOND, 1, true, KE8E7),
];
let atts = [
att(101, 1, 100),
att(102, 2, 200),
att(171, CONCLUSION, 1000),
];
assert_eq!(acceptances(&p, &plies, &atts), (true, false));
}
#[test]
fn an_offer_two_half_moves_back_is_no_longer_standing() {
let p = rook_params();
let plies = [
played(1, FIRST, 1, true, RA1A4),
played(2, SECOND, 1, false, KE8E7),
played(3, FIRST, 2, false, RA4A5),
];
let atts = [
att(101, 1, 100),
att(102, 2, 200),
att(103, 3, 300),
att(171, CONCLUSION, 1000),
];
assert_eq!(acceptances(&p, &plies, &atts), (false, false));
}
#[test]
fn an_offer_on_a_ply_that_lost_its_slot_never_stood() {
let p = rook_params();
let plies = [
played(1, FIRST, 1, false, RA1A4),
played(2, FIRST, 1, true, RA1A5),
];
let atts = [
att(101, 1, 100),
att(102, 2, 200),
att(171, CONCLUSION, 1000),
];
assert_eq!(acceptances(&p, &plies, &atts), (false, false));
}
#[test]
fn a_reply_the_selection_skipped_does_not_decline_the_offer() {
let p = rook_params();
let plies = [
played(1, FIRST, 1, true, RA1A4),
played(2, SECOND, 1, false, KE8E6),
];
let atts = [
att(101, 1, 100),
att(102, 2, 200),
att(171, CONCLUSION, 1000),
];
assert_eq!(acceptances(&p, &plies, &atts), (false, true));
}
#[test]
fn a_reply_after_the_cutoff_does_not_decline_the_offer() {
let p = rook_params();
let plies = [
played(1, FIRST, 1, true, RA1A4),
played(2, SECOND, 1, false, KE8E7),
];
let atts = [
att(101, 1, 100),
att(102, 2, 900),
att(171, CONCLUSION, 500),
];
assert_eq!(acceptances(&p, &plies, &atts), (false, true));
}
}