#![warn(clippy::pedantic)]
#![allow(
clippy::too_many_lines,
clippy::missing_errors_doc,
clippy::large_enum_variant,
clippy::module_name_repetitions
)]
use cetkaik_fundamental::AbsoluteSide;
use cetkaik_fundamental::AbsoluteSide::{ASide, IASide};
use cetkaik_traits::IsAbsoluteField;
use cetkaik_traits::IsBoard;
use cetkaik_traits::IsField;
use cetkaik_traits::{CetkaikRepresentation, IsPieceWithSide};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Season {
Iei2,
Xo1,
Kat2,
Iat1,
}
pub mod message;
pub mod probabilistic;
fn field_is_empty_at<T: CetkaikRepresentation>(
f: &T::AbsoluteField,
coord: T::AbsoluteCoord,
) -> bool {
T::as_board_absolute(f).peek(coord).is_none()
}
fn field_is_occupied_at<T: CetkaikRepresentation>(
f: &T::AbsoluteField,
coord: T::AbsoluteCoord,
) -> bool {
T::as_board_absolute(f).peek(coord).is_some()
}
fn piece_on_field_at<T: CetkaikRepresentation>(
f: &T::AbsoluteField,
coord: T::AbsoluteCoord,
) -> Option<T::AbsolutePiece> {
T::as_board_absolute(f).peek(coord)
}
impl Season {
#[must_use]
pub const fn next(self) -> Option<Self> {
match self {
Self::Iei2 => Some(Self::Xo1),
Self::Xo1 => Some(Self::Kat2),
Self::Kat2 => Some(Self::Iat1),
Self::Iat1 => None,
}
}
#[must_use]
pub const fn to_index(self) -> usize {
match self {
Self::Iei2 => 0,
Self::Xo1 => 1,
Self::Kat2 => 2,
Self::Iat1 => 3,
}
}
}
pub mod state;
impl<T: CetkaikRepresentation> state::ExcitedState_<T> {
#[must_use]
pub fn piece_at_flying_piece_src(&self) -> T::AbsolutePiece {
piece_on_field_at::<T>(&self.c.f, self.c.flying_piece_src)
.expect("Invalid `state::ExcitedState`: at `flying_piece_src` there is no piece")
}
#[must_use]
pub fn piece_at_flying_piece_step(&self) -> T::AbsolutePiece {
piece_on_field_at::<T>(&self.c.f, self.c.flying_piece_step)
.expect("Invalid `state::ExcitedState`: at `flying_piece_step` there is no piece")
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum Rate {
X1,
X2,
X4,
X8,
X16,
X32,
X64,
}
use probabilistic::Probabilistic;
impl Rate {
#[must_use]
pub const fn next(self) -> Self {
match self {
Self::X1 => Self::X2,
Self::X2 => Self::X4,
Self::X4 => Self::X8,
Self::X8 => Self::X16,
Self::X16 => Self::X32,
Self::X32 | Self::X64 => Self::X64,
}
}
#[must_use]
pub const fn num(self) -> i32 {
match self {
Self::X1 => 1,
Self::X2 => 2,
Self::X4 => 4,
Self::X8 => 8,
Self::X16 => 16,
Self::X32 => 32,
Self::X64 => 64,
}
}
}
fn apply_tam_move<T: CetkaikRepresentation>(
old_state: &state::GroundState_<T>,
src: T::AbsoluteCoord,
first_dest: T::AbsoluteCoord,
second_dest: T::AbsoluteCoord,
step: Option<T::AbsoluteCoord>,
config: Config,
) -> Result<Probabilistic<state::HandNotResolved_<T>>, &'static str> {
let (penalty1, is_a_hand1) = if old_state.tam_has_moved_previously {
match config.moving_tam_immediately_after_tam_has_moved {
Consequence::Allowed => (0, false),
Consequence::Penalized{penalty, is_a_hand} => (penalty, is_a_hand),
Consequence::Forbidden => return Err(
"By config, it is prohibited for tam2 to move immediately after the previous player has moved the tam2."
)
}
} else {
(0, false)
};
let (penalty2, is_a_hand2) =
if src == second_dest {
match config.tam_mun_mok {
Consequence::Forbidden => return Err(
"By config, it is prohibited for tam2 to start and end at the same position.",
),
Consequence::Allowed => (0, false),
Consequence::Penalized { penalty, is_a_hand } => (penalty, is_a_hand),
}
} else {
(0, false)
};
let mut new_field = old_state.f.clone();
let expect_tam = T::as_board_mut_absolute(&mut new_field)
.pop(src)
.ok_or("expected tam2 but found an empty square")?;
if expect_tam != T::absolute_tam2() {
return Err("expected tam2 but found a non-tam2 piece");
}
if field_is_occupied_at::<T>(&new_field, first_dest) {
return Err("the first destination is already occupied");
}
if let Some(st) = step {
if field_is_empty_at::<T>(&new_field, st) {
return Err("the stepping square is empty");
}
}
if field_is_occupied_at::<T>(&new_field, second_dest) {
return Err("the second destination is already occupied");
}
T::as_board_mut_absolute(&mut new_field).put(second_dest, Some(T::absolute_tam2()));
Ok(Probabilistic::Pure(state::HandNotResolved_ {
previous_a_side_hop1zuo1: old_state.f.hop1zuo1_of(ASide).collect(),
previous_ia_side_hop1zuo1: old_state.f.hop1zuo1_of(IASide).collect(),
kut2tam2_happened: false,
tam2tysak2_raw_penalty: penalty1 + penalty2,
tam2tysak2_will_trigger_taxottymok: is_a_hand1 || is_a_hand2,
rate: old_state.rate,
i_have_moved_tam_in_this_turn: true,
season: old_state.season,
scores: old_state.scores,
whose_turn: old_state.whose_turn,
f: new_field,
}))
}
fn apply_nontam_move<T: CetkaikRepresentation>(
old_state: &state::GroundState_<T>,
src: T::AbsoluteCoord,
dest: T::AbsoluteCoord,
step: Option<T::AbsoluteCoord>,
config: Config,
) -> Result<Probabilistic<state::HandNotResolved_<T>>, &'static str> {
let nothing_happened = state::HandNotResolved_ {
previous_a_side_hop1zuo1: old_state.f.hop1zuo1_of(ASide).collect(),
previous_ia_side_hop1zuo1: old_state.f.hop1zuo1_of(IASide).collect(),
kut2tam2_happened: !config.failure_to_complete_the_move_means_exempt_from_kut2_tam2
&& step.map_or(false, |step| {
T::as_board_absolute(&old_state.f).peek(step) == Some(T::absolute_tam2())
}),
rate: old_state.rate,
i_have_moved_tam_in_this_turn: false,
season: old_state.season,
scores: old_state.scores,
whose_turn: old_state.whose_turn,
f: old_state.f.clone(),
tam2tysak2_will_trigger_taxottymok: false,
tam2tysak2_raw_penalty: 0,
};
if let Some(st) = step {
if field_is_empty_at::<T>(&old_state.f, st) {
return Err("expected a stepping square but found an empty square");
}
}
let src_piece: T::AbsolutePiece =
piece_on_field_at::<T>(&old_state.f, src).ok_or("src does not contain a piece")?;
let new_field = old_state
.f
.move_nontam_piece_from_src_to_dest_while_taking_opponent_piece_if_needed(
src,
dest,
old_state.whose_turn,
)?;
let success = state::HandNotResolved_ {
previous_a_side_hop1zuo1: old_state.f.hop1zuo1_of(ASide).collect(),
previous_ia_side_hop1zuo1: old_state.f.hop1zuo1_of(IASide).collect(),
kut2tam2_happened: step.map_or(false, |step| {
piece_on_field_at::<T>(&old_state.f, step) == Some(T::absolute_tam2())
}),
rate: old_state.rate,
i_have_moved_tam_in_this_turn: false,
season: old_state.season,
scores: old_state.scores,
whose_turn: old_state.whose_turn,
f: new_field,
tam2tysak2_will_trigger_taxottymok: false,
tam2tysak2_raw_penalty: 0,
};
if !T::is_water_absolute(src)
&& !src_piece.has_prof(cetkaik_fundamental::Profession::Nuak1)
&& T::is_water_absolute(dest)
{
return Ok(Probabilistic::Water {
failure: nothing_happened,
success,
});
}
Ok(Probabilistic::Pure(success))
}
pub fn no_move_possible_at_all<T: CetkaikRepresentation>(
old_state: &state::GroundState_<T>,
config: Config,
) -> Result<state::HandResolved_<T>, &'static str> {
let (hop1zuo1_candidates, candidates) = old_state.get_candidates(config);
if hop1zuo1_candidates.is_empty() && candidates.is_empty() {
Ok(state::HandResolved_::GameEndsWithoutTymokTaxot(
old_state.scores.which_side_is_winning(),
))
} else {
Err("At least one valid move exists")
}
}
pub fn apply_normal_move<T: CetkaikRepresentation>(
old_state: &state::GroundState_<T>,
msg: message::NormalMove_<T::AbsoluteCoord>,
config: Config,
) -> Result<Probabilistic<state::HandNotResolved_<T>>, &'static str> {
let (hop1zuo1_candidates, candidates) = old_state.get_candidates(config);
match msg {
message::NormalMove_::NonTamMoveFromHopZuo { color, prof, dest } => {
let new_field = old_state
.f
.search_from_hop1zuo1_and_parachute_at(color, prof, old_state.whose_turn, dest)
.ok_or("Cannot find an adequate piece to place, or the destination is occupied")?;
if !hop1zuo1_candidates.contains(&message::PureMove__::NormalMove(msg)) {
unreachable!("inconsistencies found between cetkaik_yhuap_move_candidates::PureMove::NonTamMoveFromHopZuo and cetkaik_full_state_transition::apply_nontam_move");
}
Ok(Probabilistic::Pure(state::HandNotResolved_ {
previous_a_side_hop1zuo1: old_state.f.hop1zuo1_of(ASide).collect(),
previous_ia_side_hop1zuo1: old_state.f.hop1zuo1_of(IASide).collect(),
kut2tam2_happened: false,
rate: old_state.rate,
i_have_moved_tam_in_this_turn: false,
season: old_state.season,
scores: old_state.scores,
whose_turn: old_state.whose_turn,
f: new_field,
tam2tysak2_will_trigger_taxottymok: false,
tam2tysak2_raw_penalty: 0,
}))
}
message::NormalMove_::TamMoveNoStep {
src,
first_dest,
second_dest,
} => {
if candidates.contains(&message::PureMove__::NormalMove(msg)) {
apply_tam_move::<T>(old_state, src, first_dest, second_dest, None, config)
} else {
Err("The provided TamMoveNoStep was rejected by the crate `cetkaik_yhuap_move_candidates`.")
}
}
message::NormalMove_::TamMoveStepsDuringFormer {
src,
first_dest,
second_dest,
step,
} => {
if candidates.contains(&message::PureMove__::NormalMove(msg)) {
apply_tam_move::<T>(old_state, src, first_dest, second_dest, Some(step), config)
} else {
Err("The provided TamMoveStepsDuringFormer was rejected by the crate `cetkaik_yhuap_move_candidates`.")
}
}
message::NormalMove_::TamMoveStepsDuringLatter {
src,
first_dest,
second_dest,
step,
} => {
if candidates.contains(&message::PureMove__::NormalMove(msg)) {
apply_tam_move(old_state, src, first_dest, second_dest, Some(step), config)
} else {
Err("The provided TamMoveStepsDuringLatter was rejected by the crate `cetkaik_yhuap_move_candidates`.")
}
}
message::NormalMove_::NonTamMoveSrcDst { src, dest } => {
if candidates.contains(&message::PureMove__::NormalMove(msg)) {
apply_nontam_move(old_state, src, dest, None, config)
} else {
Err("The provided NonTamMoveSrcDst was rejected by the crate `cetkaik_yhuap_move_candidates`.")
}
}
message::NormalMove_::NonTamMoveSrcStepDstFinite { src, step, dest } => {
if candidates.contains(&message::PureMove__::NormalMove(msg)) {
apply_nontam_move(old_state, src, dest, Some(step), config)
} else {
Err("The provided NonTamMoveSrcStepDstFinite was rejected by the crate `cetkaik_yhuap_move_candidates`.")
}
}
}
}
pub fn apply_inf_after_step<T: CetkaikRepresentation + Clone>(
old_state: &state::GroundState_<T>,
msg: message::InfAfterStep_<T::AbsoluteCoord>,
config: Config,
) -> Result<Probabilistic<state::ExcitedState_<T>>, &'static str> {
if field_is_empty_at::<T>(&old_state.f, msg.src) {
return Err("In InfAfterStep, `src` is not occupied; illegal");
}
if field_is_empty_at::<T>(&old_state.f, msg.step) {
return Err("In InfAfterStep, `step` is not occupied; illegal");
}
let (_hop1zuo1, candidates) = old_state.get_candidates(config);
if !candidates.into_iter().any(|cand| match cand {
message::PureMove__::InfAfterStep(message::InfAfterStep_ {
src,
step,
planned_direction: _,
}) => src == msg.src && step == msg.step,
message::PureMove__::NormalMove(_) => false,
}) {
return Err(
"The provided InfAfterStep was rejected by the crate `cetkaik_yhuap_move_candidates`.",
);
}
let c: state::ExcitedStateWithoutCiurl_<T> = state::ExcitedStateWithoutCiurl_ {
f: old_state.f.clone(),
whose_turn: old_state.whose_turn,
flying_piece_src: msg.src,
flying_piece_step: msg.step,
flying_piece_planned_direction: msg.planned_direction,
season: old_state.season,
scores: old_state.scores,
rate: old_state.rate,
};
Ok(Probabilistic::Sticks {
s0: state::ExcitedState_ {
c: c.clone(),
ciurl: 0,
},
s1: state::ExcitedState_ {
c: c.clone(),
ciurl: 1,
},
s2: state::ExcitedState_ {
c: c.clone(),
ciurl: 2,
},
s3: state::ExcitedState_ {
c: c.clone(),
ciurl: 3,
},
s4: state::ExcitedState_ {
c: c.clone(),
ciurl: 4,
},
s5: state::ExcitedState_ { c, ciurl: 5 },
})
}
mod score;
pub use score::Scores;
pub fn apply_after_half_acceptance<T: CetkaikRepresentation>(
old_state: &state::ExcitedState_<T>,
msg: message::AfterHalfAcceptance_<T::AbsoluteCoord>,
config: Config,
) -> Result<Probabilistic<state::HandNotResolved_<T>>, &'static str> {
let nothing_happened = state::HandNotResolved_ {
previous_a_side_hop1zuo1: old_state.c.f.hop1zuo1_of(ASide).collect(),
previous_ia_side_hop1zuo1: old_state.c.f.hop1zuo1_of(IASide).collect(),
kut2tam2_happened: !config.failure_to_complete_the_move_means_exempt_from_kut2_tam2
&& old_state.piece_at_flying_piece_step() == T::absolute_tam2(),
rate: old_state.c.rate,
i_have_moved_tam_in_this_turn: false,
season: old_state.c.season,
scores: old_state.c.scores,
whose_turn: old_state.c.whose_turn,
f: old_state.c.f.clone(),
tam2tysak2_will_trigger_taxottymok: false,
tam2tysak2_raw_penalty: 0,
};
let candidates = old_state.get_candidates(config);
if !candidates.contains(&msg) {
return Err(
"The provided InfAfterStep was rejected either by the crate `cetkaik_yhuap_move_candidates`, or because the ciurl limit was exceeded.",
);
}
if let Some(dest) = msg.dest {
let piece = old_state.piece_at_flying_piece_src();
let new_field = old_state
.c
.f
.move_nontam_piece_from_src_to_dest_while_taking_opponent_piece_if_needed(
old_state.c.flying_piece_src,
dest,
old_state.c.whose_turn,
)?;
let success = state::HandNotResolved_ {
previous_a_side_hop1zuo1: old_state.c.f.hop1zuo1_of(ASide).collect(),
previous_ia_side_hop1zuo1: old_state.c.f.hop1zuo1_of(IASide).collect(),
kut2tam2_happened: old_state.piece_at_flying_piece_step() == T::absolute_tam2(),
rate: old_state.c.rate,
i_have_moved_tam_in_this_turn: false,
season: old_state.c.season,
scores: old_state.c.scores,
whose_turn: old_state.c.whose_turn,
f: new_field,
tam2tysak2_will_trigger_taxottymok: false,
tam2tysak2_raw_penalty: 0,
};
if !T::is_water_absolute(old_state.c.flying_piece_src)
&& !piece.has_prof(cetkaik_fundamental::Profession::Nuak1)
&& T::is_water_absolute(dest)
{
Ok(Probabilistic::Water {
success,
failure: nothing_happened,
})
} else {
Ok(Probabilistic::Pure(success))
}
} else {
Ok(Probabilistic::Pure(nothing_happened))
}
}
pub use score::Victor;
#[derive(Clone, Debug)]
pub enum IfTaxot_<T: CetkaikRepresentation> {
NextSeason(Probabilistic<state::GroundState_<T>>),
VictoriousSide(Victor),
}
#[readonly::make]
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
pub struct Config {
pub step_tam_is_a_hand: bool,
pub tam_itself_is_tam_hue: bool,
pub moving_tam_immediately_after_tam_has_moved: Consequence,
pub tam_mun_mok: Consequence,
pub failure_to_complete_the_move_means_exempt_from_kut2_tam2: bool,
pub game_can_end_without_tymok_taxot_because_of_negative_hand: bool,
pub what_to_say_before_casting_sticks: Option<Plan>,
}
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
pub enum Plan {
Direction,
ExactDestination,
}
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
pub enum Consequence {
Allowed,
Penalized { penalty: i32, is_a_hand: bool },
Forbidden,
}
impl Config {
#[must_use]
pub const fn cerke_online_alpha() -> Self {
Self {
step_tam_is_a_hand: false,
tam_itself_is_tam_hue: true,
moving_tam_immediately_after_tam_has_moved: Consequence::Forbidden,
tam_mun_mok: Consequence::Allowed,
failure_to_complete_the_move_means_exempt_from_kut2_tam2: false,
game_can_end_without_tymok_taxot_because_of_negative_hand: true,
what_to_say_before_casting_sticks: Some(Plan::Direction),
}
}
#[must_use]
pub const fn strict_y1_huap1() -> Self {
Self {
step_tam_is_a_hand: true,
tam_itself_is_tam_hue: false,
moving_tam_immediately_after_tam_has_moved: Consequence::Penalized {
penalty: -3,
is_a_hand: true,
},
tam_mun_mok: Consequence::Penalized {
penalty: -3,
is_a_hand: true,
},
failure_to_complete_the_move_means_exempt_from_kut2_tam2: false,
game_can_end_without_tymok_taxot_because_of_negative_hand: false,
what_to_say_before_casting_sticks: Some(Plan::ExactDestination),
}
}
}
#[must_use]
pub fn resolve<T: CetkaikRepresentation + Clone>(
state: &state::HandNotResolved_<T>,
config: Config,
) -> state::HandResolved_<T> {
use cetkaik_calculate_hand::{calculate_hands_and_score_from_pieces, ScoreAndHands};
let tymoxtaxot_because_of_kut2tam2 = state.kut2tam2_happened && config.step_tam_is_a_hand;
let tymoxtaxot_because_of_newly_acquired: Option<i32> = match state.whose_turn {
AbsoluteSide::ASide => {
if state.previous_a_side_hop1zuo1 == state.f.hop1zuo1_of(state.whose_turn).collect::<Vec<_>>() {
None
} else {
let ScoreAndHands {
score: _,
hands: old_hands,
} = calculate_hands_and_score_from_pieces(&state.previous_a_side_hop1zuo1)
.expect("cannot fail, since the supplied list of piece should not exceed the limit on the number of piece");
let ScoreAndHands {
score: new_score,
hands: new_hands,
} = calculate_hands_and_score_from_pieces(&state.f.hop1zuo1_of(state.whose_turn).collect::<Vec<_>>())
.expect("cannot fail, since the supplied list of piece should not exceed the limit on the number of piece");
if new_hands.difference(&old_hands).next().is_some() {
Some(new_score)
} else {
None
}
}
}
AbsoluteSide::IASide => {
if state.previous_ia_side_hop1zuo1 == state.f.hop1zuo1_of(state.whose_turn).collect::<Vec<_>>() {
None
} else {
let ScoreAndHands {
score: _,
hands: old_hands,
} = calculate_hands_and_score_from_pieces(&state.previous_ia_side_hop1zuo1)
.expect("cannot fail, since the supplied list of piece should not exceed the limit on the number of piece");
let ScoreAndHands {
score: new_score,
hands: new_hands,
} = calculate_hands_and_score_from_pieces(&state.f.hop1zuo1_of(state.whose_turn).collect::<Vec<_>>())
.expect("cannot fail, since the supplied list of piece should not exceed the limit on the number of piece");
if new_hands.difference(&old_hands).count() > 0 {
Some(new_score)
} else {
None
}
}
}
};
if !tymoxtaxot_because_of_kut2tam2
&& tymoxtaxot_because_of_newly_acquired.is_none()
&& !state.tam2tysak2_will_trigger_taxottymok
{
match state
.scores
.edit(state.tam2tysak2_raw_penalty, state.whose_turn, state.rate)
{
Ok(new_scores) => {
return state::HandResolved_::NeitherTymokNorTaxot(state::GroundState_ {
f: state.f.clone(),
whose_turn: !state.whose_turn,
season: state.season,
scores: new_scores,
rate: state.rate,
tam_has_moved_previously: state.i_have_moved_tam_in_this_turn,
});
}
Err(victor) => return state::HandResolved_::GameEndsWithoutTymokTaxot(victor),
}
}
let raw_score = state.tam2tysak2_raw_penalty
+ if tymoxtaxot_because_of_kut2tam2 {
-5
} else {
0
}
+ tymoxtaxot_because_of_newly_acquired.unwrap_or(0);
let if_taxot = match state.scores.edit(raw_score, state.whose_turn, state.rate) {
Err(victor) => IfTaxot_::VictoriousSide(victor),
Ok(new_scores) => {
state.season.next().map_or(
IfTaxot_::VictoriousSide(new_scores.which_side_is_winning()),
|next_season| IfTaxot_::NextSeason(beginning_of_season(next_season, new_scores)),
)
}
};
state::HandResolved_::HandExists {
if_tymok: state::GroundState_ {
f: state.f.clone(),
whose_turn: !state.whose_turn,
season: state.season,
scores: state.scores,
rate: state.rate.next(),
tam_has_moved_previously: state.i_have_moved_tam_in_this_turn,
},
if_taxot,
}
}
#[must_use]
pub fn initial_state<T: CetkaikRepresentation + Clone>() -> Probabilistic<state::GroundState_<T>> {
beginning_of_season(Season::Iei2, Scores::new())
}
fn beginning_of_season<T: CetkaikRepresentation + Clone>(
season: Season,
scores: Scores,
) -> Probabilistic<state::GroundState_<T>> {
let ia_first = state::GroundState_ {
whose_turn: AbsoluteSide::IASide,
scores,
rate: Rate::X1,
season,
tam_has_moved_previously: false,
f: <T::AbsoluteField as IsAbsoluteField>::yhuap_initial(),
};
Probabilistic::WhoGoesFirst {
a_first: state::GroundState_ {
whose_turn: AbsoluteSide::ASide,
..ia_first.clone()
},
ia_first,
}
}