battler 0.9.0

Pokémon battle engine for Rust.
Documentation
use anyhow::Result;
use battler::{
    BattleType,
    CoreBattleEngineSpeedSortTieResolution,
    PublicCoreBattle,
    TeamData,
    WrapResultError,
};
use battler_test_utils::{
    LogMatch,
    TestBattleBuilder,
    assert_logs_since_turn_eq,
    static_local_data_store,
};

fn team() -> Result<TeamData> {
    serde_json::from_str(
        r#"{
            "members": [
                {
                    "name": "Pikachu",
                    "species": "Pikachu",
                    "ability": "No Ability",
                    "moves": [
                        "Tackle"
                    ],
                    "nature": "Hardy",
                    "level": 50
                },
                {
                    "name": "Magikarp",
                    "species": "Magikarp",
                    "ability": "No Ability",
                    "moves": [
                        "Splash"
                    ],
                    "nature": "Hardy",
                    "level": 50
                },
                {
                    "name": "Eevee",
                    "species": "Eevee",
                    "ability": "No Ability",
                    "moves": [
                        "Tackle"
                    ],
                    "nature": "Hardy",
                    "level": 50
                }
            ]
        }"#,
    )
    .wrap_error()
}

fn make_battle(seed: u64, team_1: TeamData, team_2: TeamData) -> Result<PublicCoreBattle<'static>> {
    TestBattleBuilder::new()
        .with_battle_type(BattleType::Doubles)
        .with_seed(seed)
        .with_team_validation(false)
        .with_pass_allowed(true)
        .with_speed_sort_tie_resolution(CoreBattleEngineSpeedSortTieResolution::Keep)
        .add_player_to_side_1("player-1", "Player 1")
        .add_player_to_side_2("player-2", "Player 2")
        .with_team("player-1", team_1)
        .with_team("player-2", team_2)
        .build(static_local_data_store())
}

#[test]
fn outside_effects_trigger_immediately_at_start_of_turn() {
    let mut battle = make_battle(0, team().unwrap(), team().unwrap()).unwrap();
    assert_matches::assert_matches!(battle.start(), Ok(()));

    assert_matches::assert_matches!(
        battle.push_outside_effect(
            serde_json::from_str(
                r#"{
                    "name": "Toxic Fumes",
                    "target": "field",
                    "program": [
                        "foreach $mon in func_call(all_active_mons):",
                        [
                            "if func_call(chance: 1 2):",
                            [
                                "set_status: $mon tox"
                            ]
                        ]
                    ]
                }"#,
            )
            .unwrap(),
        ),
        Ok(())
    );

    assert_matches::assert_matches!(
        battle.set_player_choice("player-1", "switch 2;move 0"),
        Ok(())
    );
    assert_matches::assert_matches!(
        battle.set_player_choice("player-2", "move 0,1;move 0"),
        Ok(())
    );

    let expected_logs = serde_json::from_str::<Vec<LogMatch>>(
        r#"[
            "status|mon:Pikachu,player-1,1|status:Bad Poison|from:Toxic Fumes",
            "status|mon:Magikarp,player-1,2|status:Bad Poison|from:Toxic Fumes",
            "status|mon:Magikarp,player-2,2|status:Bad Poison|from:Toxic Fumes",
            "split|side:0",
            ["switch", "player-1", "Eevee"],
            ["switch", "player-1", "Eevee"],
            "move|mon:Pikachu,player-2,1|name:Tackle|target:Eevee,player-1,1",
            "split|side:0",
            "damage|mon:Eevee,player-1,1|health:96/115",
            "damage|mon:Eevee,player-1,1|health:84/100",
            "move|mon:Magikarp,player-1,2|name:Splash|target:Magikarp,player-1,2",
            "activate|move:Splash",
            "move|mon:Magikarp,player-2,2|name:Splash|target:Magikarp,player-2,2",
            "activate|move:Splash",
            "split|side:0",
            "damage|mon:Magikarp,player-1,2|from:status:Bad Poison|health:75/80",
            "damage|mon:Magikarp,player-1,2|from:status:Bad Poison|health:94/100",
            "split|side:1",
            "damage|mon:Magikarp,player-2,2|from:status:Bad Poison|health:75/80",
            "damage|mon:Magikarp,player-2,2|from:status:Bad Poison|health:94/100",
            "residual",
            "turn|turn:2"
        ]"#,
    )
    .unwrap();
    assert_logs_since_turn_eq(&battle, 1, &expected_logs);
}

#[test]
fn outside_effect_targets_individual_mon() {
    let mut battle = make_battle(0, team().unwrap(), team().unwrap()).unwrap();
    assert_matches::assert_matches!(battle.start(), Ok(()));

    assert_matches::assert_matches!(
        battle.push_outside_effect(
            serde_json::from_str(
                r#"{
                    "name": "Faint",
                    "target": {
                        "mon": {
                            "player": "player-2",
                            "position": 0
                        }
                    },
                    "program": [
                        "faint: $target"
                    ]
                }"#,
            )
            .unwrap(),
        ),
        Ok(())
    );

    assert_matches::assert_matches!(
        battle.set_player_choice("player-1", "move 0,1;move 0"),
        Ok(())
    );
    assert_matches::assert_matches!(
        battle.set_player_choice("player-2", "move 0,1;move 0"),
        Ok(())
    );

    let expected_logs = serde_json::from_str::<Vec<LogMatch>>(
        r#"[
            "faint|mon:Pikachu,player-2,1",
            "move|mon:Pikachu,player-1,1|name:Tackle|target:Magikarp,player-2,2",
            "split|side:1",
            "damage|mon:Magikarp,player-2,2|health:62/80",
            "damage|mon:Magikarp,player-2,2|health:78/100",
            "move|mon:Magikarp,player-1,2|name:Splash|target:Magikarp,player-1,2",
            "activate|move:Splash",
            "move|mon:Magikarp,player-2,2|name:Splash|target:Magikarp,player-2,2",
            "activate|move:Splash",
            "residual"
        ]"#,
    )
    .unwrap();
    assert_logs_since_turn_eq(&battle, 1, &expected_logs);
}

#[test]
fn outside_conditions_can_be_applied_by_outside_effect() {
    let mut battle = make_battle(0, team().unwrap(), team().unwrap()).unwrap();
    assert_matches::assert_matches!(battle.start(), Ok(()));

    assert_matches::assert_matches!(
        battle.push_outside_condition(
            serde_json::from_str(
                r#"{
                    "name": "Stat Shuffle",
                    "condition_type": "Condition",
                    "condition": {
                        "callbacks": {
                            "on_field_start": ["log_field_start"],
                            "on_field_residual": {
                                "priority": 1,
                                "program": ["log_field_activate"]
                            },
                            "on_residual": [
                                "boost: $target str('{}:{}', func_call(sample: func_call(boostable_stats)), func_call(sample: [-1, 1]))"
                            ],
                            "on_field_end": ["log_field_end"]
                        }
                    }
                }"#,
            )
            .unwrap(),
        ),
        Ok(())
    );
    assert_matches::assert_matches!(
        battle.push_outside_effect(
            serde_json::from_str(
                r#"{
                    "name": "Stat Shuffle",
                    "target": "field",
                    "program": [
                        "add_pseudo_weather: statshuffle"
                    ]
                }"#,
            )
            .unwrap(),
        ),
        Ok(())
    );

    assert_matches::assert_matches!(battle.set_player_choice("player-1", "pass;pass"), Ok(()));
    assert_matches::assert_matches!(battle.set_player_choice("player-2", "pass;pass"), Ok(()));

    let expected_logs = serde_json::from_str::<Vec<LogMatch>>(
        r#"[
            "fieldstart|condition:Stat Shuffle",
            "fieldactivate|condition:Stat Shuffle",
            "unboost|mon:Pikachu,player-1,1|stat:atk|by:1|from:condition:Stat Shuffle",
            "unboost|mon:Pikachu,player-2,1|stat:spd|by:1|from:condition:Stat Shuffle",
            "unboost|mon:Magikarp,player-1,2|stat:spa|by:1|from:condition:Stat Shuffle",
            "boost|mon:Magikarp,player-2,2|stat:spa|by:1|from:condition:Stat Shuffle",
            "residual",
            "turn|turn:2"
        ]"#,
    )
    .unwrap();
    assert_logs_since_turn_eq(&battle, 1, &expected_logs);
}