diff --git a/lib/diamondtail/brain.ex b/lib/diamondtail/brain.ex index fbd46db..4068727 100644 --- a/lib/diamondtail/brain.ex +++ b/lib/diamondtail/brain.ex @@ -55,7 +55,6 @@ defmodule Diamondtail.Brain do |> Map.update(:snakes, nil, fn snakes -> Map.update(snakes, state.self, nil, &apply_action_to_snake(&1, move)) end) - # |> resolve_state() end def apply_actions(%GameState{type: :q} = state, moves) do @@ -84,17 +83,22 @@ defmodule Diamondtail.Brain do end end - def valid_actions(%GameState{type: :q, snakes: snakes, self: self}) do + def valid_actions(%GameState{type: :q, snakes: snakes, self: self} = state) do non_self_snakes = Map.delete(snakes, self) for i <- 0..(4 ** map_size(non_self_snakes)) - 1 do Enum.with_index(non_self_snakes) |> Enum.map(fn {{id, _}, j} -> {id, Enum.at([:up, :down, :left, :right], rem(floor(i / (4 ** j)), 4))} end) + |> Enum.filter(fn {id, action} -> + result = apply_actions(state, %{id => action}) + !snake_dies?(result, id) + end) |> Map.new() end end + @spec evaluate_state(Genome.t(), GameState.t(), non_neg_integer(), float() | nil, float() | nil) :: float() def evaluate_state(genome, state, depth, alpha \\ Float.min_finite, beta \\ Float.max_finite) def evaluate_state(genome, %GameState{type: :v} = state, 0, _, _) do # If we're dead, the result is always super duper negative @@ -104,11 +108,14 @@ defmodule Diamondtail.Brain do self = state.snakes[state.self] Enum.count(enemies) * genome.enemy_alive_weight + + Enum.count(enemies, &(&1.length >= self.length)) * genome.enemy_longer_weight + self.health * genome.own_health_weight + self.length * genome.own_length_weight + + length(available_tiles_from(state, self.head)) * genome.own_accessible_tiles_weight + Enum.sum_by(enemies, &(&1.health)) * genome.enemy_health_weight + (Enum.sum_by(enemies, &(&1.length)) / (Enum.count(enemies) + 1)) * genome.enemy_length_weight + - (Enum.map(enemies, &(abs(&1.head.x - self.head.x) + abs(&1.head.y - self.head.y))) |> Enum.min(&<=/2, fn -> 0 end)) * genome.enemy_head_distance_weight + + (Enum.flat_map(enemies, &(if &1.length >= self.length, do: [abs(&1.head.x - self.head.x) + abs(&1.head.y - self.head.y)], else: [])) |> Enum.min(&<=/2, fn -> 0 end)) * genome.longer_enemy_head_distance_weight + + (Enum.flat_map(enemies, &(if &1.length < self.length, do: [abs(&1.head.x - self.head.x) + abs(&1.head.y - self.head.y)], else: [])) |> Enum.min(&<=/2, fn -> 0 end)) * genome.shorter_enemy_head_distance_weight + (Enum.map(food, &(abs(&1.x - self.head.x) + abs(&1.y - self.head.y))) |> Enum.min(&<=/2, fn -> 0 end)) * genome.food_distance_weight else Float.min_finite() @@ -130,9 +137,10 @@ defmodule Diamondtail.Brain do valid_actions = valid_actions(state) if valid_actions == [] do # Terminal state + # We only win if this is a Q state AND we're still alive, this might still be a draw case state.type do :v -> Float.min_finite() - :q -> Float.max_finite() + :q -> if snake_dies?(state, state.self), do: IO.inspect(Float.min_finite()), else: Float.max_finite() end else {score, _alpha, _beta} = valid_actions(state) @@ -188,15 +196,19 @@ defmodule Diamondtail.Brain do defp snake_dies?(state, snake_id) do snake = state.snakes[snake_id] - # Die if health is less than or equal to 0 - snake.health <= 0 || - # Die if head is out of bounds - snake.head.x < 0 || snake.head.x >= state.width || - snake.head.y < 0 || snake.head.y >= state.height || - # Die if head is in a tail - Enum.any?(state.snakes, fn {_, %{body: body}} -> Enum.member?(body, snake.head) end) || - # Die if head is on head of a DIFFERENT snake of equal or greater length - Enum.any?(state.snakes, fn {id, %{head: h, length: l}} -> snake.head == h && snake.length <= l && id != snake_id end) + if snake == nil do + true + else + # Die if health is less than or equal to 0 + snake.health <= 0 || + # Die if head is out of bounds + snake.head.x < 0 || snake.head.x >= state.width || + snake.head.y < 0 || snake.head.y >= state.height || + # Die if head is in a tail + Enum.any?(state.snakes, fn {_, %{body: body}} -> Enum.member?(body, snake.head) end) || + # Die if head is on head of a DIFFERENT snake of equal or greater length + Enum.any?(state.snakes, fn {id, %{head: h, length: l}} -> snake.head == h && l >= snake.length && id != snake_id end) + end end defp apply_action_to_snake(snake, move) do @@ -211,6 +223,22 @@ defmodule Diamondtail.Brain do |> Map.update(:body, nil, fn body -> List.delete_at(body, 0) ++ [snake.head] end) |> Map.update(:head, nil, fn %{x: x, y: y} -> %{x: x + delta.x, y: y + delta.y} end) end + + def available_tiles_from(state, %{x: x, y: y} = base_pos, explored \\ []) do + Enum.reduce([%{x: x + 1, y: y}, %{x: x - 1, y: y}, %{x: x, y: y + 1}, %{x: x, y: y - 1}], [base_pos | explored], fn pos, acc -> + if Enum.member?(acc, pos) || in_solid_or_out_of_bounds?(state, pos) do + acc + else + available_tiles_from(state, pos, acc) + end + end) + end + + defp in_solid_or_out_of_bounds?(state, %{x: x, y: y} = pos) do + x >= state.width || y >= state.height || + x < 0 || y < 0 || + Enum.any?(state.snakes, fn {_, %{body: b, head: h}} -> h == pos || Enum.member?(b, pos) end) + end end @spec determine_move(Genome.t(), map(), map()) :: %{move: :up | :down | :left | :right, shout: String.t()} diff --git a/lib/diamondtail/population.ex b/lib/diamondtail/population.ex index 7cca268..788d731 100644 --- a/lib/diamondtail/population.ex +++ b/lib/diamondtail/population.ex @@ -15,12 +15,15 @@ defmodule Diamondtail.Population do field :mutation_max_amplitude, float() field :enemy_alive_weight, float() + field :enemy_longer_weight, float() field :own_health_weight, float() field :own_length_weight, float() field :enemy_health_weight, float() field :enemy_length_weight, float() - field :enemy_head_distance_weight, float() + field :longer_enemy_head_distance_weight, float() + field :shorter_enemy_head_distance_weight, float() field :food_distance_weight, float() + field :own_accessible_tiles_weight, float() field :search_depth, float() field :value_discount, float() @@ -61,17 +64,21 @@ defmodule Diamondtail.Population do mutation_chance: random_between(0.1, 0.2), mutation_max_amplitude: random_between(0.2, 0.05), enemy_alive_weight: random_between(-100.0, -200.0), + enemy_longer_weight: random_between(-50.0, -30.0), # Try to stay longer than our enemies to avoid dying to head-to-head collisions own_health_weight: random_between(0.7, 1.0), own_length_weight: random_between(5, 10), enemy_health_weight: random_between(-0.8, -0.4), # Summed enemy_length_weight: random_between(-10, -5), # Averaged # both of the following are compared to the nearest object, to avoid overrewarding when there are more enemies/foods - enemy_head_distance_weight: random_between(1, 2), # maximize distance, reward staying away - food_distance_weight: random_between(-2, -1), # minimize distance, reward staying near + longer_enemy_head_distance_weight: random_between(0.3, 1), # maximize distance, reward staying away + shorter_enemy_head_distance_weight: random_between(-1, -0.3), # minimize distance, reward staying near to try to go in for kills + food_distance_weight: random_between(-0.5, -0.1), # minimize distance, reward staying near, don't just stick next to food though - search_depth: random_between(3.0, 7.0), - value_discount: random_between(1.0, 10.0) # as percentage + own_accessible_tiles_weight: random_between(0.7, 1), + + search_depth: random_between(3.0, 5.0), + value_discount: random_between(1.0, 20.0) # as percentage } end