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
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
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()