def test_direct_move_removes_and_returns_cpu(player, enemy):
    cpu_used = 2
    time = 3
    damage = 1
    move = Move(
        direct_damage(damage, cpu_slots=cpu_used, time_to_resolve=time),
        player, enemy)

    logic = CombatLogic([player, enemy])

    def get_cpu():
        return player.status.get_attribute(Attributes.CPU_AVAILABLE)

    def get_health():
        return enemy.status.get_attribute(Attributes.HEALTH)

    starting_cpu = get_cpu()
    starting_health = get_health()
    # Add move to stack and wait until it resolves.
    logic.start_round([move])
    for _ in range(time):
        assert get_health() == starting_health
        logic.end_round()
        assert get_cpu() == starting_cpu - cpu_used
        logic.start_round([])
        assert get_cpu() == starting_cpu - cpu_used

    # When it resolves cpu should return and HP should go down.
    logic.end_round()
    assert get_cpu() == starting_cpu
    assert get_health() == starting_health - damage
Example #2
0
    def __init__(self,
                 enemies: Sequence[Character] = None,
                 win_resolution: Resolution = None,
                 loss_resolution: Resolution = None,
                 background_image: str = None) -> None:
        if enemies is None:
            enemies = (build_character(data=CharacterTypes.DRONE.data), )
        self._enemies: Tuple[Character, ...] = tuple(enemies)
        super().__init__()
        self._player = get_player()

        self._combat_logic = CombatLogic((self._player, ) + self._enemies)

        self._win_resolution = win_resolution
        self._loss_resolution = loss_resolution

        self._selected_char: Optional[Character] = None

        self._update_layout()

        # Rect positions
        if background_image is None:
            self._background_image = BackgroundImages.CITY.path
        else:
            self._background_image = background_image

        # Animation data
        self._animation_progress: Optional[float] = None
        self._first_turn = True
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 test_combat_logic_initializes_cpu(player, enemy):
    chars = [player, enemy]
    for char in chars:
        cpu = char.status.get_attribute(Attributes.CPU_AVAILABLE)
        char.status.increment_attribute(Attributes.CPU_AVAILABLE, -cpu)

    CombatLogic(chars)

    for char in chars:
        actual = char.status.get_attribute(Attributes.CPU_AVAILABLE)
        expected = char.status.get_attribute(Attributes.MAX_CPU)
        assert actual == expected
Example #5
0
class CombatScene(EventListener, Scene):
    """Represents and updates all model data involved during a combat."""
    def __init__(self,
                 enemies: Sequence[Character] = None,
                 win_resolution: Resolution = None,
                 loss_resolution: Resolution = None,
                 background_image: str = None) -> None:
        if enemies is None:
            enemies = (build_character(data=CharacterTypes.DRONE.data), )
        self._enemies: Tuple[Character, ...] = tuple(enemies)
        super().__init__()
        self._player = get_player()

        self._combat_logic = CombatLogic((self._player, ) + self._enemies)

        self._win_resolution = win_resolution
        self._loss_resolution = loss_resolution

        self._selected_char: Optional[Character] = None

        self._update_layout()

        # Rect positions
        if background_image is None:
            self._background_image = BackgroundImages.CITY.path
        else:
            self._background_image = background_image

        # Animation data
        self._animation_progress: Optional[float] = None
        self._first_turn = True

    @property
    def animation_progress(self) -> Optional[float]:
        """Progress of a combat scene animation.

        This variable is None if no animation is in progress.
        """
        return self._animation_progress

    @property
    def layout(self) -> Layout:
        return self._layout

    @property
    def background_image(self) -> str:
        return self._background_image

    def available_moves(self) -> List[Move]:
        """All player moves that may be added to the combat stack.

        Returns all moves that may be added to the stack, accounting for the
        current target and available CPU slots.
        """
        if self._selected_char is None:
            return [Move(_wait_one_round, self._player, self._player)]
        return _valid_moves(self._player, [self._selected_char])

    def is_resolved(self) -> bool:
        if all(is_dead(c) for c in self._enemies):
            return True

        return is_dead(self._player)

    def get_resolution(self) -> Resolution:
        assert self.is_resolved()
        if all(is_dead(e) for e in self._enemies):
            assert self._win_resolution is not None, (
                'win resolution unspecified at init.')
            return self._win_resolution
        assert is_dead(self._player)
        assert self._loss_resolution is not None, (
            'loss resolution unspecified at init.')
        return self._loss_resolution

    def notify(self, event: EventType) -> None:
        if isinstance(event, SelectCharacterEvent):
            # Cannot select dead characters
            char = event.character
            if char in self._combat_logic.active_characters() or char is None:
                self._selected_char = char
                self._update_layout()

        # Add new moves to the combat stack and start animation
        if isinstance(event, SelectPlayerMoveEvent):

            moves = [event.move]
            active_chars = self._combat_logic.active_characters()
            moves.extend(
                e.ai.select_move(active_chars) for e in self._enemies
                if e in active_chars)
            self._combat_logic.start_round(moves)
            self._selected_char = None
            self._update_layout()

            # If animation enabled, start progress. Otherwise execute moves.
            if ANIMATION and not self._first_turn:
                self._animation_progress = 0.0
            else:
                self._combat_logic.end_round()
                self._update_layout()
            self._first_turn = False

        # Animation in progress
        if event == BasicEvents.TICK and self._animation_progress is not None:
            self._animation_progress += 1.0 / _ticks_per_animation
            # Execute moves once animation is finished
            if self._animation_progress >= 1.0:
                self._animation_progress = None
                self._combat_logic.end_round()
                self._update_layout()

    def __str__(self) -> str:
        return 'CombatScene(enemy = {})'.format(str(self._enemies))

    def _update_layout(self) -> None:
        """Update the screen layout according to moves and characters in scene.
        """

        # The layout is broken up into 3 columns:
        # 1. Player column, which shows player image and stats.
        # 2. Stack column, which shows moves on the stack and those which have
        # just resolved.
        # 3. Enemy column, which shows enemy images and stats.
        # We populate these columns with objects whose attributes (data) are
        # required to render the scene.
        characters = (self._player, ) + self._enemies
        all_moves = self._combat_logic.all_moves_present()

        # player side layout
        player = characters[0]

        player_layout = self._character_layout(player, all_moves)
        left_column = Layout([(None, 1), (player_layout, 1), (None, 1)],
                             'vertical')

        # stack layout
        # unresolved moves (and time to resolve)
        moves_times = self._combat_logic.stack.moves_times_remaining()[::-1]
        num_moves = len(moves_times)
        stack_size = 6
        stack_elems: List[Tuple[Any, int]] = [(MoveInfo(*m_t), 1)
                                              for m_t in moves_times]
        # Add a gap rect so that rects are always scaled to the same size by
        # the layout.
        if num_moves <= stack_size:
            stack_elems.append((None, stack_size - num_moves))

        unresolved = Layout(stack_elems, 'vertical')
        unresolved = Layout([(None, 1), (unresolved, 5), (None, 1)],
                            'horizontal')

        # resolved moves
        resolved_size = 4
        resolved_moves = self._combat_logic.stack.resolved_moves()[::-1]
        resolved_elems: List[Tuple[Any, int]] = [(MoveInfo(mv, 0), 1)
                                                 for mv in resolved_moves]
        # Add a gap to ensure consistent rect sizes.
        if len(resolved_moves) < resolved_size:
            resolved_elems.append((None, resolved_size - len(resolved_moves)))

        resolved = Layout(resolved_elems, 'vertical')
        resolved = Layout([(None, 1), (resolved, 5), (None, 1)], 'horizontal')

        middle_column = Layout([(None, 4), (unresolved, stack_size), (None, 2),
                                (resolved, resolved_size), (None, 6)])

        # enemies layout
        assert len(characters) > 1

        right_elements: List[Tuple[Any, int]] = [(None, 1)]
        for enemy in characters[1:]:
            enemy_layout = self._character_layout(enemy, all_moves)

            right_elements.extend([(enemy_layout, 2), (None, 1)])

        right_column = Layout(right_elements, 'vertical')

        self._layout = Layout([(left_column, 2), (middle_column, 3),
                               (right_column, 2)], 'horizontal', SCREEN_SIZE)

    def _character_layout(self, char: Character,
                          all_moves: Sequence[Move]) -> Layout:

        char_layout = Layout([(None, 1), (self._character_info(char), 2),
                              (None, 1)], 'horizontal')

        move_space = 3
        # Pull out all unique moves by the character
        moves_set = {m for m in all_moves if m.user is char}
        moves = [MoveInfo(m, 0, True) for m in moves_set]

        move_layout = Layout([(m, 1) for m in moves])
        move_layout = Layout([(None, 1), (move_layout, 4), (None, 1)],
                             'horizontal')
        full_elements = [(char_layout, 5), (None, 3),
                         (move_layout, min(move_space, len(moves)))]
        if len(moves) < move_space:
            full_elements.append((None, move_space - len(moves)))

        return Layout(full_elements)

    def _character_info(self, char: Character) -> CharacterInfo:
        def attr_value(attr: Attributes) -> int:
            return char.status.get_attribute(attr)

        return CharacterInfo(char, attr_value(Attributes.SHIELD),
                             attr_value(Attributes.HEALTH),
                             attr_value(Attributes.MAX_HEALTH),
                             attr_value(Attributes.CPU_AVAILABLE),
                             attr_value(Attributes.MAX_CPU),
                             tuple(char.status.active_effects()),
                             char.description(), char.image_path,
                             is_dead(char), char is self._selected_char)
def test_move_with_multi_turn_use(player, enemy, time_to_resolve):
    num_rounds = 3
    damage_per_round = 1
    cpu_used = 1
    move = Move(
        damage_over_time(damage_per_round,
                         num_rounds,
                         cpu_used,
                         time_to_resolve=time_to_resolve), player, enemy)

    logic = CombatLogic([player, enemy])

    def get_cpu():
        return player.status.get_attribute(Attributes.CPU_AVAILABLE)

    def get_health():
        return enemy.status.get_attribute(Attributes.HEALTH)

    starting_cpu = get_cpu()
    starting_health = get_health()
    # Add move to stack and wait until it resolves.
    logic.start_round([move])
    assert get_cpu() == starting_cpu

    # Wait rounds to resolve
    for _ in range(time_to_resolve):
        assert get_health() == starting_health
        logic.end_round()
        assert get_cpu() == starting_cpu - cpu_used
        logic.start_round([])

    for rnd in range(num_rounds - 1):
        assert get_health() == starting_health - rnd * damage_per_round
        logic.end_round()
        assert get_cpu() == starting_cpu - cpu_used
        assert get_health() == starting_health - (rnd + 1) * damage_per_round
        logic.start_round([])
        assert get_cpu() == starting_cpu - cpu_used

    # When it finishes cpu should return and HP should go down.
    logic.end_round()
    assert get_cpu() == starting_cpu
    assert get_health() == starting_health - num_rounds * damage_per_round
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()