def test_after_effect_occurs_at_end_of_move(player, enemy, multi_use, duration,
                                            time_to_resolve):
    after_effect_calls = []

    def after_effect(user, target):
        after_effect_calls.append(True)

    move = Move(
        build_subroutine(time_to_resolve=time_to_resolve,
                         duration=duration,
                         multi_use=multi_use,
                         after_effect=after_effect), player, enemy)

    logic = CombatLogic([player, enemy])

    logic.start_round([move])
    assert not after_effect_calls

    for _ in range(time_to_resolve + duration):
        logic.end_round()
        assert not after_effect_calls
        logic.start_round([])
        assert not after_effect_calls

    logic.end_round()
    assert after_effect_calls
def direct_damage(damage: int,
                  cpu_slots: int = None,
                  time_to_resolve: int = None,
                  label: str = '') -> Subroutine:
    """Subroutine to deal direct damage.

    The subroutine's user cannot target itself.

    Args:
        damage: Damage dealt, must be non-negative.
        cpu_slots: CPU slots required. Default is
            1 + damage // (2 * time_to_resolve+1).
        time_to_resolve:  Time to resolve. Default is int(sqrt(damage))
        label: A label prepended to the subroutine description.
    """

    if damage < 0:
        raise ValueError('damage must be non-negative.')
    if time_to_resolve is None:
        time_to_resolve = int(math.sqrt(damage))
    if cpu_slots is None:
        cpu_slots = 1 + damage // (2 * time_to_resolve + 1)

    def use_fun(user: Character, target: Character) -> None:
        damage_target(damage, target)

    def can_use_fun(user: Character, target: Character) -> bool:
        return not same_team(user, target)

    description = '{} damage'.format(damage, time_to_resolve)
    if label:
        description = label + ' ' + description
    return build_subroutine(use_fun, can_use_fun, cpu_slots, time_to_resolve,
                            description)
Esempio n. 3
0
def _heal_over_time() -> Subroutine:
    def use_fun(user: Character, target: Character) -> None:
        target.status.increment_attribute(Attributes.HEALTH, 1)

    def can_use(user: Character, target: Character) -> bool:
        health = target.status.get_attribute(Attributes.HEALTH)
        max_health = target.status.get_attribute(Attributes.MAX_HEALTH)
        return same_team(user, target) and health < max_health

    rounds = 3
    desc = 'repair +1 for {} rounds'.format(rounds)

    return build_subroutine(use_fun, can_use, 1, 1, desc, rounds - 1, True)
def damage_over_time(damage_per_round: int,
                     num_rounds: int = 2,
                     cpu_slots: int = None,
                     time_to_resolve: int = None,
                     label: str = '') -> Subroutine:
    """Subroutine to deal damage over multiple rounds.

    The subroutine's user cannot target itself.

    Args:
        damage_per_round: Damage dealt per round, must be non-negative.
        num_rounds: The number of rounds that the damage is dealt. Default is 2.
            Must be positive.
        cpu_slots: CPU slots required. Default is
            1 + damage // (2 * time_to_resolve+1).
        time_to_resolve:  Number of rounds before first damage is dealt.
            Default is int(sqrt(damage))
        label: A label prepended to the subroutine description.
    """

    if damage_per_round < 0:
        raise ValueError('damage must be non-negative.')
    if num_rounds <= 0:
        raise ValueError('duration must be positive.')

    if time_to_resolve is None:
        time_to_resolve = int(math.sqrt(damage_per_round))

    if cpu_slots is None:
        # CPU should grow with DPS, with less CPU for larger total time
        total_damage = damage_per_round * num_rounds
        total_time = time_to_resolve + num_rounds
        cpu_slots = 1 + total_damage // (2 * total_time)

    def use_fun(user: Character, target: Character) -> None:
        damage_target(damage_per_round, target)

    def can_use_fun(user: Character, target: Character) -> bool:
        return not same_team(user, target)

    description = '{} damage/{} turns'.format(damage_per_round * num_rounds,
                                              num_rounds)
    if label:
        description = label + ' ' + description
    return build_subroutine(use_fun,
                            can_use_fun,
                            cpu_slots,
                            time_to_resolve,
                            description,
                            num_rounds - 1,
                            multi_use=True)
Esempio n. 5
0
    def test_no_valid_moves_gives_default_move(self, ai_type):
        unusable = build_subroutine(can_use=False)
        data = CharacterData(ChassisData(subroutines_granted=(unusable,)))
        user = build_character(data=data)
        ai = build_ai(ai_type)
        ai.set_user(user)
        target = build_character(data=data)

        move_comps = set()
        for _ in range(100):
            move = ai.select_move([target])
            sub = move.subroutine
            components = (
                sub.cpu_slots(), sub.time_to_resolve(), sub.description())
            move_comps.add(components)

        assert len(move_comps) == 1
def repair(amount: int) -> Subroutine:
    """Self repair subroutine.

    Args:
        amount: Amount to repair. Must be positive.

    """
    assert amount > 0

    def use_fun(user: Character, target: Character) -> None:
        target.status.increment_attribute(Attributes.HEALTH, amount)

    cpu_slots = max(1, amount // 2)
    time_slots = max(1, amount // 2)

    description = 'Repair {}'.format(amount)

    return build_subroutine(use_fun, user_is_target, cpu_slots, time_slots,
                            description)
def shield_buff(amount: int,
                num_rounds: int = 1,
                cpu_slots: int = None,
                time_to_resolve: int = 0) -> Subroutine:
    """Adds a temporary damage shield buffer to the user or an ally.

    The shield fizzles either at the end of combat.

    Args:
        amount: Amount of shield added in a round. Must be non-negative.
        num_rounds: Number of rounds the buff is invoked. The total shield value
            does stack. Must be positive.
        cpu_slots: CPU slots required. By default this is
            max(floor(sqrt(amount * duration) - sqrt(time_to_resolve)), 0)
        time_to_resolve: Time before shield occurs. By default the shield is
            instantaneous. Must be non-negative.
    """

    if amount < 0:
        raise ValueError('shield amount must be non-negative')
    if num_rounds < 1:
        raise ValueError('shield duration must be positive.')
    if time_to_resolve < 0:
        raise ValueError('shield time to resolve must be non-negative.')

    def use_fun(user: Character, target: Character) -> None:
        # Shield cannot decrease nor can it be made larger than amount.
        target.status.increment_attribute(Attributes.SHIELD, amount)

    if cpu_slots is None:
        cpu = math.sqrt(amount * num_rounds) - math.sqrt(time_to_resolve)
        cpu_slots = max(0, int(cpu))

    description = '+{} shield'.format(amount)
    if num_rounds > 1:
        description += ' for {} rounds'.format(num_rounds)
    return build_subroutine(use_fun,
                            same_team,
                            cpu_slots,
                            time_to_resolve,
                            description,
                            num_rounds - 1,
                            multi_use=True)
def test_move_disappears_after_expected_time(player, enemy, multi_use,
                                             duration, time_to_resolve):
    move = Move(
        build_subroutine(time_to_resolve=time_to_resolve,
                         duration=duration,
                         multi_use=multi_use), player, enemy)

    logic = CombatLogic([player, enemy])

    logic.start_round([move])

    # A unique copy of the move is created so that it may be properly tracked.
    assert len(logic.all_moves_present()) == 1
    move = logic.all_moves_present()[0]

    for _ in range(time_to_resolve + duration):
        logic.end_round()
        assert move in logic.all_moves_present()
        logic.start_round([])
        assert move in logic.all_moves_present()

    logic.end_round()
    assert move not in logic.all_moves_present()
Esempio n. 9
0
from events.events_base import (BasicEvents, EventListener, EventType,
                                SelectCharacterEvent, SelectPlayerMoveEvent)
from models.characters.character_base import Character
from models.characters.character_examples import CharacterTypes
from models.characters.character_impl import build_character
from models.characters.conditions import is_dead
from models.characters.moves_base import Move
from models.characters.player import get_player
from models.characters.states import Attributes, StatusEffect
from models.characters.subroutines_base import build_subroutine
from models.combat.combat_logic import CombatLogic
from models.scenes.layouts import Layout
from models.scenes.scenes_base import Resolution, Scene

_wait_one_round = build_subroutine(can_use=True,
                                   num_cpu=0,
                                   time_to_resolve=1,
                                   description='wait one round')

_ANIMATION_TIME_SECONDS = 1.0
_ticks_per_animation = FRAMES_PER_SECOND * _ANIMATION_TIME_SECONDS
ANIMATION = True  # Set to False during integration tests


def _valid_moves(user: Character, targets: Sequence[Character]) -> List[Move]:
    """All valid moves from a user to a sequence of targets, ignoring CPU slots.
    """
    return [
        Move(sub, user, target)
        for sub, target in product(user.chassis.all_subroutines(), targets)
        if sub.can_use(user, target) and
        sub.cpu_slots() <= user.status.get_attribute(Attributes.CPU_AVAILABLE)
def adjust_attribute(attribute: Attributes,
                     amount: int = 0,
                     duration: int = 0,
                     cpu_slots: int = None,
                     time_to_resolve: int = 0,
                     is_buff: bool = None) -> Subroutine:
    """Adjust a permanent attribute over a finite duration.

    Permanent attributes may not be reduced below zero.

    Args:
        attribute: Attribute to be modified. attribute.is_permanent must be
            True.
        amount: How much to change the attribute by. May be positive or
            negative.
        duration: Number of rounds over which attribute is modified. Must be
            non-negative.
        cpu_slots: Number of CPU slots required to maintain the adjustment.
            Default is math.abs(amount).
        time_to_resolve: Number of rounds before modification is applied.
        is_buff: Whether the adjustment is a 'buff' (i.e. helpful). If True,
            then it may only be used on allies. If False, it may only be used
            on enemies. By default the subroutine may be used on both.
    """
    if duration < 0:
        raise ValueError('duration must be non-negative.')
    if not attribute.is_permanent:
        raise ValueError('Only permanent attributes may be modified.')

    description = '{} {}'.format(amount, attribute.value)
    if amount >= 0:
        description = '+' + description

    effect = StatusEffect.build(description,
                                attribute_modifiers={attribute: amount})

    def use_fun(user: Character, target: Character) -> None:
        target.status.add_status_effect(effect)

    def can_use(user: Character, target: Character) -> bool:
        if is_buff is None:
            return True
        elif is_buff:
            return same_team(user, target)
        else:
            return not same_team(user, target)

    def after_effect(user: Character, target: Character) -> None:
        # If some other subroutine removes the effect first, this can cause
        # an error.
        target.status.remove_status_effect(effect)

    if cpu_slots is None:
        cpu_slots = abs(amount)

    description += ' {} rounds'.format(duration)

    return build_subroutine(use_fun,
                            can_use,
                            cpu_slots,
                            time_to_resolve,
                            description,
                            duration,
                            multi_use=False,
                            after_effect=after_effect)
Esempio n. 11
0
_shoot_laser = direct_damage(2, label='small laser')

_SINGLE_LASER = ChassisData(slot_capacities={
    SlotTypes.HEAD: 1,
    SlotTypes.STORAGE: 1
},
                            states_granted=(State.ON_FIRE, ),
                            attribute_modifiers={
                                Attributes.MAX_HEALTH: 5,
                                Attributes.MAX_CPU: 2
                            },
                            subroutines_granted=(_shoot_laser, ))

_do_nothing_1 = build_subroutine(can_use=True,
                                 description='Do nothing',
                                 time_to_resolve=1,
                                 num_cpu=0)
_do_nothing_2 = build_subroutine(can_use=True,
                                 description='Do nothing',
                                 time_to_resolve=2,
                                 num_cpu=0)
_unusable = build_subroutine(can_use=False)

_HARMLESS = ChassisData(attribute_modifiers={
    Attributes.MAX_HEALTH: 1,
    Attributes.MAX_CPU: 1
},
                        subroutines_granted=(_do_nothing_1, _do_nothing_2,
                                             _unusable))

_USELESS = ChassisData(attribute_modifiers={
"""Tests for CombatStack."""
from unittest.mock import Mock

import pytest

from models.characters.moves_base import Move
from models.characters.subroutines_base import build_subroutine
from models.combat.combat_stack import CombatStack

time_1_sub = build_subroutine(time_to_resolve=1)
time_2_sub = build_subroutine(time_to_resolve=2)
time_3_sub = build_subroutine(time_to_resolve=3)

move_1 = Mock(Move, name='1 turns', subroutine=time_1_sub)
move_2 = Mock(Move, name='2 turns', subroutine=time_2_sub)
move_3_A = Mock(Move, name='3 turns A', subroutine=time_3_sub)
move_3_B = Mock(Move, name='3 turns B', subroutine=time_3_sub)


def test_resolved_moves_after_init():
    stack = CombatStack()

    assert stack.resolved_moves() == ()


def test_moves_remaining_correct_order():
    stack = CombatStack()

    for m in [move_2, move_3_A, move_3_B, move_1]:
        stack.add_move(m, m.subroutine.time_to_resolve())
Esempio n. 13
0
def test_subroutine_eq():
    assert build_subroutine() != build_subroutine()
    sub = build_subroutine()
    assert sub == sub
    assert sub != sub.copy()