diplomacy 0.1.1

Adjudication library for the board game Diplomacy
Documentation
use super::calc::{dislodger_of, is_head_to_head, max_prevent_result, path_exists};
use super::convoy::ConvoyOutcome;
use super::resolver::{Context, ResolverState};
use super::support::{self, SupportOutcome};
use super::{Adjudicate, MappedMainOrder, OrderOutcome, OrderState};
use crate::geo::Terrain;
use crate::judge::strength::{Prevent, Strength};
use crate::order::Command;
use crate::ShortName;

/// The standard Diplomacy rules.
#[derive(Debug, Clone, Default)]
pub struct Rulebook;

impl Rulebook {
    /// Apply rules to determine hold outcome.
    fn adjudicate_hold<'a>(
        ctx: &Context<'a, Self>,
        rslv: &mut ResolverState<'a>,
        ord: &'a MappedMainOrder,
    ) -> HoldOutcome<'a> {
        dislodger_of(ctx, rslv, ord)
            .map(HoldOutcome::Dislodged)
            .unwrap_or(HoldOutcome::Succeeds)
    }

    /// Apply rules to determine move outcome.
    fn adjudicate_move<'a>(
        ctx: &Context<'a, Self>,
        rslv: &mut ResolverState<'a>,
        ord: &'a MappedMainOrder,
    ) -> AttackOutcome<'a> {
        if ord.command.move_dest() == Some(&ord.region) {
            AttackOutcome::MoveToSelf
        } else if !path_exists(ctx, rslv, ord) {
            AttackOutcome::NoPath
        } else if ord.command.is_move() {
            let mut atk_supports = support::find_for(ctx, rslv, ord);
            let mut atk_strength = 1 + atk_supports.len();
            let prevent = max_prevent_result(ctx, rslv, ord);

            // if the attack cannot overcome the prevent even in the best case,
            // there isn't any point in continuing the calculation and we should
            // immediately report the failure. This avoids breaking test case
            // 6.C.03 Three army circular movement bounces.
            if atk_strength <= prevent.strength() {
                AttackOutcome::Prevented(prevent.unwrap())
            } else {
                if let Some(occupier) =
                    ctx.find_order_to_province(ord.command.move_dest().unwrap().into())
                {
                    // A head-to-head battle occurs when two units have mirrored move orders and
                    // no convoy is available to help one of the units move around the other.
                    let is_head_to_head = is_head_to_head(ctx, rslv, ord, occupier);

                    let resistance = if !occupier.command.is_move() || is_head_to_head {
                        // DEFEND and HOLD strengths include supports that may seek to thwart
                        // other orders from the same nation.

                        // Example:
                        // France:
                        // A Belgium Supports A Burgundy - Ruhr
                        // A Holland Supports A Burgundy - Ruhr
                        // A Burgundy - Ruhr
                        // A Munich Supports A Ruhr - Burgundy
                        // A Marseilles - Burgundy

                        // Germany:
                        // A Ruhr - Burgundy
                        // In this example the French army in Munich supports the move of the German army
                        // in Ruhr instead of the French army in Burgundy. This makes that the ATTACK STRENGTH,
                        // the PREVENT STRENGTH and the DEFEND STRENGTH of the German army in Ruhr are all different.
                        // The ATTACK STRENGTH is one, because the French support should not be counted for the attack.
                        // The PREVENT STRENGTH is zero, because it is dislodged by the French army in Burgundy
                        // and therefore it can not prevent the army in Marseilles to go to Burgundy. However, the
                        // DEFEND STRENGTH contains all supports and is therefore two. Still this DEFEND STRENGTH
                        // is insufficient in the head to head battle, since the French army in Burgundy has an
                        // ATTACK STRENGTH of three.
                        1 + support::find_for(ctx, rslv, occupier).len()
                    }
                    // failed exits resist with strength 1 (the unit trapped in the province)
                    else if rslv.resolve(ctx, occupier) == OrderState::Fails {
                        1
                    // successful exits mount no resistance
                    } else {
                        0
                    };

                    // A unit can not dislodge a unit of the same player.
                    // Head-to-head, failed exit, and hold cases all collapse in friendly fire.
                    if resistance > 0 && ord.nation == occupier.nation {
                        return AttackOutcome::FriendlyFire;
                    } else if resistance > 0 {
                        // Supports to a foreign unit can not be used to dislodge an own unit.
                        // Therefore, we remove any move supports from the nation whose unit
                        // is resisting the move.
                        atk_supports = atk_supports
                            .into_iter()
                            .filter(|sup| sup.nation != occupier.nation)
                            .collect();
                        atk_strength = 1 + atk_supports.len();

                        if atk_strength <= resistance {
                            if is_head_to_head {
                                return AttackOutcome::LostHeadToHead;
                            } else {
                                return AttackOutcome::OccupierDefended;
                            }
                        }
                    }
                }

                AttackOutcome::Succeeds
            }
        } else {
            panic!("Don't try to adjudicate non-moves as moves");
        }
    }

    fn adjudicate_support<'a>(
        ctx: &Context<'a, Self>,
        rslv: &mut ResolverState<'a>,
        ord: &'a MappedMainOrder,
    ) -> SupportOutcome<'a> {
        if support::is_supporting_self(ord) {
            SupportOutcome::SupportingSelf
        } else if !support::can_reach(ctx.world_map, ord) {
            SupportOutcome::CantReach
        } else {
            match support::find_cutting_order(ctx, rslv, ord) {
                Some(cutter) => SupportOutcome::CutBy(cutter),
                None => SupportOutcome::NotDisrupted,
            }
        }
    }

    fn adjudicate_convoy<'a>(
        ctx: &Context<'a, Self>,
        rslv: &mut ResolverState<'a>,
        ord: &'a MappedMainOrder,
    ) -> ConvoyOutcome<'a> {
        // Test case 6.F.1: Fleets cannot convoy in coastal areas
        //
        // Note: We explicitly check that "coast" is none because explicit-coast
        // regions are marked as being 'sea' to prevent armies from occupying them,
        // but are not valid locations for convoys to operate.
        let is_at_sea = ord.region.coast().is_none()
            && ctx
                .world_map
                .find_region(&ord.region.short_name())
                .map(|r| r.terrain() == Terrain::Sea)
                .unwrap_or(false);

        if !is_at_sea {
            return ConvoyOutcome::NotAtSea;
        }

        if let Some(dislodger) = dislodger_of(ctx, rslv, ord) {
            return ConvoyOutcome::Dislodged(dislodger);
        }

        if rslv.order_in_paradox(ord) {
            ConvoyOutcome::Paradox
        } else {
            ConvoyOutcome::NotDisrupted
        }
    }
}

impl Adjudicate for Rulebook {
    fn adjudicate<'a>(
        &self,
        context: &Context<'a, Self>,
        resolver: &mut ResolverState<'a>,
        order: &'a MappedMainOrder,
    ) -> OrderState {
        self.explain(context, resolver, order).into()
    }

    fn explain<'a>(
        &self,
        context: &Context<'a, Self>,
        resolver: &mut ResolverState<'a>,
        order: &'a MappedMainOrder,
    ) -> OrderOutcome<'a> {
        use crate::order::MainCommand::*;

        if let Some(reason) = resolver.invalid_orders.get(order) {
            return OrderOutcome::Invalid(*reason);
        }

        match order.command {
            // A move order succeeds when the unit successfully transitions to the target.
            Move(..) => Rulebook::adjudicate_move(context, resolver, order).into(),

            // A support order "succeeds" if the support is not cut. This doesn't
            // necessarily mean support got applied.
            Support(..) => Rulebook::adjudicate_support(context, resolver, order).into(),

            // Hold orders succeed when the unit is not dislodged.
            Hold => Rulebook::adjudicate_hold(context, resolver, order).into(),

            // Convoy orders succeed when the unit is not dislodged and the convoy doesn't create
            // a paradox.
            Convoy(..) => Rulebook::adjudicate_convoy(context, resolver, order).into(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HoldOutcome<'a> {
    /// The unit remains in its current region
    Succeeds,
    /// The unit is dislodged by the specified order
    Dislodged(&'a MappedMainOrder),
}

impl From<&'_ HoldOutcome<'_>> for OrderState {
    fn from(other: &HoldOutcome<'_>) -> Self {
        if other == &HoldOutcome::Succeeds {
            OrderState::Succeeds
        } else {
            OrderState::Fails
        }
    }
}

impl From<HoldOutcome<'_>> for OrderState {
    fn from(other: HoldOutcome<'_>) -> Self {
        (&other).into()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttackOutcome<'a> {
    MoveToSelf,
    NoPath,
    FriendlyFire,
    Prevented(Prevent<'a>),
    /// The intended victim of the attack instead dislodged the attacker and did not use a convoy.
    ///
    /// A unit that loses a head-to-head battle is dislodged, cannot retreat to the province from
    /// which it was attacked, and has no strength to prevent other units from occupying that
    /// province.
    LostHeadToHead,
    /// The intended victim of the attack fended off the attacker, possibly with support from
    /// other units.
    OccupierDefended,
    Succeeds,
}

impl From<&'_ AttackOutcome<'_>> for OrderState {
    fn from(ao: &AttackOutcome) -> Self {
        if ao == &AttackOutcome::Succeeds {
            OrderState::Succeeds
        } else {
            OrderState::Fails
        }
    }
}

impl From<AttackOutcome<'_>> for OrderState {
    fn from(ao: AttackOutcome) -> Self {
        (&ao).into()
    }
}

impl<'a> From<AttackOutcome<'a>> for bool {
    fn from(ao: AttackOutcome) -> Self {
        OrderState::from(ao).into()
    }
}

impl PartialEq<OrderState> for AttackOutcome<'_> {
    fn eq(&self, os: &OrderState) -> bool {
        OrderState::from(self) == *os
    }
}