Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-404] lowest and highest stat targeting strategies #647

Merged
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 61 additions & 8 deletions apps/champions/lib/champions/battle/simulator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ defmodule Champions.Battle.Simulator do
[x] Frontline - Heroes in slots 1 and 2
[x] Backline - Heroes in slots 2 to 4
[x] All
[ ] Self
[x] Self
[ ] Factions
[ ] Classes
[ ] Min (STAT)
[ ] Max (STAT)
[x] Lowest (STAT)
[x] Highest (STAT)

It can also be chosen how many targets are affected by the effect, and if they are allies or enemies.

Expand Down Expand Up @@ -621,6 +621,34 @@ defmodule Champions.Battle.Simulator do
[caster.id]
end

defp choose_targets_by_strategy(
caster,
%{type: %{"lowest" => stat}, target_allies: target_allies, count: count},
state
Nico-Sanchez marked this conversation as resolved.
Show resolved Hide resolved
) do
target_team =
Enum.filter(state.units, fn {_id, unit} -> unit.team == caster.team == target_allies end)

Enum.map(target_team, fn {_id, unit} -> unit end)
|> sort_units_by_stat(stat, true)
Nico-Sanchez marked this conversation as resolved.
Show resolved Hide resolved
|> Enum.take(count)
|> Enum.map(fn unit -> unit.id end)
end

defp choose_targets_by_strategy(
caster,
%{type: %{"highest" => stat}, target_allies: target_allies, count: count},
state
) do
target_team =
Enum.filter(state.units, fn {_id, unit} -> unit.team == caster.team == target_allies end)

Enum.map(target_team, fn {_id, unit} -> unit end)
|> sort_units_by_stat(stat, false)
|> Enum.take(count)
|> Enum.map(fn unit -> unit.id end)
Nico-Sanchez marked this conversation as resolved.
Show resolved Hide resolved
end

defp choose_targets_by_strategy(caster, %{type: "all", target_allies: target_allies}, state),
do:
state.units
Expand Down Expand Up @@ -1069,20 +1097,29 @@ defmodule Champions.Battle.Simulator do
"all",
"frontline",
"backline",
"self"
"self",
"lowest",
"highest"
]

defp create_mechanics_map(%Mechanic{} = mechanic, skill_id, caster_id) do
targeting_strategy_type = mechanic.apply_effects_to.targeting_strategy.type

apply_effects_to = %{
effects: Enum.map(mechanic.apply_effects_to.effects, &create_effect_map(&1, skill_id)),
targeting_strategy: %{
# TODO: replace random for the corresponding target type name (CHoM #325)
# type: mechanic.apply_effects_to.targeting_strategy.type,
type:
if mechanic.apply_effects_to.targeting_strategy.type in @implemented_targeting_strategies do
mechanic.apply_effects_to.targeting_strategy.type
else
"random"
cond do
is_binary(targeting_strategy_type) && targeting_strategy_type in @implemented_targeting_strategies ->
targeting_strategy_type

hd(Map.keys(targeting_strategy_type)) in @implemented_targeting_strategies ->
targeting_strategy_type

true ->
"random"
Nico-Sanchez marked this conversation as resolved.
Show resolved Hide resolved
end,
count: mechanic.apply_effects_to.targeting_strategy.count || 1,
target_allies: mechanic.apply_effects_to.targeting_strategy.target_allies || false
Expand Down Expand Up @@ -1168,6 +1205,22 @@ defmodule Champions.Battle.Simulator do
}
end

defp sort_units_by_stat(units, stat, desc) do
Enum.sort(
units,
&cond do
calculate_unit_stat(&1, String.to_atom(stat)) > calculate_unit_stat(&2, String.to_atom(stat)) ->
ncontinanza marked this conversation as resolved.
Show resolved Hide resolved
if desc, do: false, else: true

calculate_unit_stat(&1, String.to_atom(stat)) == calculate_unit_stat(&2, String.to_atom(stat)) ->
if Enum.random([true, false]), do: true, else: false

true ->
if desc, do: true, else: false
end
)
end

defp string_to_atom("type"), do: :type
defp string_to_atom("duration"), do: :duration
defp string_to_atom("period"), do: :period
Expand Down
201 changes: 201 additions & 0 deletions apps/champions/test/battle_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,207 @@ defmodule Champions.Test.BattleTest do
assert "team_2" ==
Champions.Battle.Simulator.run_battle([self_damage_unit], [target_dummy], maximum_steps: maximum_steps).result
end

test "Lowest Health" do
# This test pairs a unit with a basic skill that deals damage to the enemy with the lowest health, against two targets.
# The first target has 10 health points and the second one has 9 health points. The enemy with lowest health can kill the opponent in one hit, so if the lowest health targeting strategy fails, the battle will end in a victory for the team_2.
# The second enemy will hit team_1 unit once, for half of its health points, so if it hits twice the battle will end in a victory for the team_2.

attacker_cooldown = 1
attacking_character_hp = 20
attacking_character_base_attack = 10
highest_enemy_hp = attacking_character_base_attack
lowest_enemy_hp = attacking_character_base_attack - 1

# Create a character with a basic skill that will deal 10 damage to all the enemies
basic_skill_params =
TestUtils.build_skill(%{
name: "DealDamage Lowest HP Enemy",
mechanics: [
%{
trigger_delay: 0,
apply_effects_to:
TestUtils.build_apply_effects_to_mechanic(%{
effects: [
TestUtils.build_effect(%{
executions: [
%{
type: "DealDamage",
attack_ratio: 1,
energy_recharge: 0
}
]
})
],
targeting_strategy: %{
type: %{"lowest" => "health"},
target_allies: false,
count: 1
}
})
}
],
cooldown: attacker_cooldown * @miliseconds_per_step
})

{:ok, character} =
TestUtils.build_character(%{
name: "Lowest HP Attacking Character",
basic_skill: basic_skill_params,
ultimate_skill: TestUtils.build_skill(%{name: "Lowest target Skill"}),
base_attack: attacking_character_base_attack,
base_health: attacking_character_hp
})
|> Characters.insert_character()

{:ok, attacking_unit} = TestUtils.build_unit(%{character_id: character.id}) |> Units.insert_unit()
{:ok, attacking_unit} = Units.get_unit(attacking_unit.id)

{:ok, enemy_character_lowest_health} =
TestUtils.build_character(%{
base_health: lowest_enemy_hp,
base_attack: attacking_character_hp,
name: "Lowest HP Target Enemy",
basic_skill:
TestUtils.build_skill(%{
name: "Lowest HP Target Enemy Basic",
cooldown: 1 + attacker_cooldown * @miliseconds_per_step
}),
ultimate_skill: TestUtils.build_skill(%{name: "Lowest HP Target Enemy Ultimate"})
})
|> Characters.insert_character()

{:ok, enemy_lowest_health} =
TestUtils.build_unit(%{character_id: enemy_character_lowest_health.id}) |> Units.insert_unit()

{:ok, enemy_lowest_health} = Units.get_unit(enemy_lowest_health.id)

{:ok, enemy_character_highest_health} =
TestUtils.build_character(%{
base_health: highest_enemy_hp,
base_attack: trunc(attacking_character_hp / 2),
name: "Highest HP Target Enemy",
basic_skill:
TestUtils.build_skill(%{
name: "Highest HP Target Enemy Basic",
cooldown: 1 + attacker_cooldown * @miliseconds_per_step
}),
ultimate_skill: TestUtils.build_skill(%{name: "Highest HP Target Enemy Ultimate"})
})
|> Characters.insert_character()

{:ok, enemy_highest_health} =
TestUtils.build_unit(%{character_id: enemy_character_highest_health.id}) |> Units.insert_unit()

{:ok, enemy_highest_health} = Units.get_unit(enemy_highest_health.id)

assert "team_1" ==
Champions.Battle.Simulator.run_battle(
[attacking_unit],
[enemy_lowest_health, enemy_highest_health],
maximum_steps: 5
).result
end

test "Highest Health" do
# This test pairs a unit with a basic skill that deals damage to the enemy with the highest health, against two targets.
# The first target has 10 health points and the second one has 9 health points. The enemy with highest health can kill the opponent in one hit, so if the highest health targeting strategy fails, the battle will end in a victory for the team_2.
# The second enemy will hit team_1 unit once, for half of its health points, so if it hits twice the battle will end in a victory for the team_2.

attacker_cooldown = 1
attacking_character_hp = 20
attacking_character_base_attack = 10
highest_enemy_hp = attacking_character_base_attack
lowest_enemy_hp = attacking_character_base_attack - 1

# Create a character with a basic skill that will deal 10 damage to all the enemies
basic_skill_params =
TestUtils.build_skill(%{
name: "DealDamage Highest HP Enemy",
mechanics: [
%{
trigger_delay: 0,
apply_effects_to:
TestUtils.build_apply_effects_to_mechanic(%{
effects: [
TestUtils.build_effect(%{
executions: [
%{
type: "DealDamage",
attack_ratio: 1,
energy_recharge: 0
}
]
})
],
targeting_strategy: %{
type: %{"highest" => "health"},
target_allies: false
}
})
}
],
cooldown: attacker_cooldown * @miliseconds_per_step
})

{:ok, character} =
TestUtils.build_character(%{
name: "Highest HP Attacking Character",
basic_skill: basic_skill_params,
ultimate_skill: TestUtils.build_skill(%{name: "Highest target ultimate Skill"}),
base_attack: attacking_character_base_attack,
base_health: attacking_character_hp
})
|> Characters.insert_character()

{:ok, attacking_unit} = TestUtils.build_unit(%{character_id: character.id}) |> Units.insert_unit()
{:ok, attacking_unit} = Units.get_unit(attacking_unit.id)

{:ok, enemy_character_lowest_health} =
TestUtils.build_character(%{
base_health: lowest_enemy_hp,
base_attack: trunc(attacking_character_hp / 2),
name: "Lowest HP Enemy",
basic_skill:
TestUtils.build_skill(%{
name: "Lowest HP Enemy Basic",
cooldown: 1 + attacker_cooldown * @miliseconds_per_step
}),
ultimate_skill: TestUtils.build_skill(%{name: "Lowest HP Enemy Ultimate"})
})
|> Characters.insert_character()

{:ok, enemy_lowest_health} =
TestUtils.build_unit(%{character_id: enemy_character_lowest_health.id}) |> Units.insert_unit()

{:ok, enemy_lowest_health} = Units.get_unit(enemy_lowest_health.id)

{:ok, enemy_character_highest_health} =
TestUtils.build_character(%{
base_health: highest_enemy_hp,
base_attack: attacking_character_hp,
name: "Highest HP Enemy",
basic_skill:
TestUtils.build_skill(%{
name: "Highest HP Enemy Basic",
cooldown: 1 + attacker_cooldown * @miliseconds_per_step
}),
ultimate_skill: TestUtils.build_skill(%{name: "Highest HP Enemy Ultimate"})
})
|> Characters.insert_character()

{:ok, enemy_highest_health} =
TestUtils.build_unit(%{character_id: enemy_character_highest_health.id}) |> Units.insert_unit()

{:ok, enemy_highest_health} = Units.get_unit(enemy_highest_health.id)

assert "team_1" ==
Champions.Battle.Simulator.run_battle(
[attacking_unit],
[enemy_lowest_health, enemy_highest_health],
maximum_steps: 5
).result
end
end

describe "Items" do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ defmodule GameBackend.Units.Skills.Mechanics.TargetingStrategies.Type do
# Needed for the case in which we decode from the DB (Further explanation in: https://hexdocs.pm/ecto/Ecto.Schema.html#embeds_one/3-encoding-and-decoding)
def cast(%{"factions" => factions}), do: {:ok, {"factions", factions}}
def cast(%{"classes" => classes}), do: {:ok, {"classes", classes}}
def cast(%{"lowest" => attribute}), do: {:ok, %{lowest: attribute}}
def cast(%{"highest" => attribute}), do: {:ok, %{highest: attribute}}
def cast(%{"lowest" => attribute}), do: {:ok, %{"lowest" => attribute}}
def cast(%{"highest" => attribute}), do: {:ok, %{"highest" => attribute}}

def cast(_), do: :error

Expand Down
Loading