diff --git a/lib/diamondtail/application.ex b/lib/diamondtail/application.ex index bf20f2c..52306ae 100644 --- a/lib/diamondtail/application.ex +++ b/lib/diamondtail/application.ex @@ -2,6 +2,7 @@ defmodule Diamondtail.Application do # See https://hexdocs.pm/elixir/Application.html # for more information on OTP Applications @moduledoc false + alias Diamondtail.ExternalLatencyManager alias Diamondtail.Genometracker alias Diamondtail.Population @@ -15,7 +16,7 @@ defmodule Diamondtail.Application do {Plug.Cowboy, scheme: :http, plug: Diamondtail.Router, options: [port: 4001]}, {Task.Supervisor, name: Diamondtail.TaskSupervisor}, {Population, 1..10 |> Enum.map(fn _ -> Population.Genome.random end) |> Enum.to_list}, - Genometracker + Genometracker, ExternalLatencyManager ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/diamondtail/brain.ex b/lib/diamondtail/brain.ex index 4068727..06d14be 100644 --- a/lib/diamondtail/brain.ex +++ b/lib/diamondtail/brain.ex @@ -1,4 +1,7 @@ defmodule Diamondtail.Brain do + import Diamondtail.Operators.Implication + alias Diamondtail.InternalLatencyManager + alias Diamondtail.ExternalLatencyManager alias Diamondtail.Population.Genome require Logger @@ -83,17 +86,13 @@ defmodule Diamondtail.Brain do end end - def valid_actions(%GameState{type: :q, snakes: snakes, self: self} = state) do + def valid_actions(%GameState{type: :q, snakes: snakes, self: self}) 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 @@ -140,7 +139,7 @@ defmodule Diamondtail.Brain do # 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 -> if snake_dies?(state, state.self), do: IO.inspect(Float.min_finite()), else: Float.max_finite() + :q -> if snake_dies?(state, state.self), do: Float.min_finite(), else: Float.max_finite() end else {score, _alpha, _beta} = valid_actions(state) @@ -241,13 +240,19 @@ defmodule Diamondtail.Brain do end end - @spec determine_move(Genome.t(), map(), map()) :: %{move: :up | :down | :left | :right, shout: String.t()} - def determine_move(genome, board, self) do + @spec determine_move(Genome.t(), map(), map(), map()) :: %{move: :up | :down | :left | :right, shout: String.t()} + def determine_move(genome, board, self, game) do base_state = GameState.from(board, self) + external_latency = ExternalLatencyManager.get_external_latency() + max_latency = game["timeout"] + target_latency = max_latency - external_latency - genome.latency_headroom + + depth = InternalLatencyManager.best_depth(map_size(base_state.snakes), target_latency) + options = GameState.valid_actions(base_state) |> Task.async_stream(fn action -> qstate = GameState.apply_actions(base_state, action) - {action, GameState.evaluate_state(genome, qstate, ceil(genome.search_depth))} + {action, GameState.evaluate_state(genome, qstate, depth)} end, ordered: false) if Enum.empty?(options) do diff --git a/lib/diamondtail/externallatencymanager.ex b/lib/diamondtail/externallatencymanager.ex new file mode 100644 index 0000000..e86d6aa --- /dev/null +++ b/lib/diamondtail/externallatencymanager.ex @@ -0,0 +1,33 @@ +defmodule Diamondtail.ExternalLatencyManager do + use Agent + + def start_link(_) do + Agent.start_link(fn -> {0, 0, %{}} end, name: __MODULE__) + end + + def report_internal_latency(move, latency) do + Agent.update(__MODULE__, fn {tot_latency, tot_datapoints, dangling_moves} -> + {tot_latency, tot_datapoints, Map.put(dangling_moves, move, latency)} + end) + end + + def report_total_latency(move, latency) do + Agent.update(__MODULE__, fn {tot_latency, tot_datapoints, dangling_moves} -> + case Map.pop(dangling_moves, move) do + {nil, dangling_moves} -> {tot_latency, tot_datapoints, dangling_moves} + {internal_latency, dangling_moves} -> {tot_latency + (latency - internal_latency), tot_datapoints + 1, dangling_moves} + end + end) + end + + def get_external_latency() do + {tot, dp} = Agent.get(__MODULE__, fn {tot, dp, _} -> {tot, dp} end) + tot / dp + end + + def clear_dangling_moves(predicate \\ fn -> true end) do + Agent.update(__MODULE__, fn {tot_latency, tot_datapoints, dangling_moves} -> + {tot_latency, tot_datapoints, Map.filter(dangling_moves, fn {k, _} -> !predicate.(k) end)} + end) + end +end diff --git a/lib/diamondtail/internallatencymanager.ex b/lib/diamondtail/internallatencymanager.ex new file mode 100644 index 0000000..1c46f7e --- /dev/null +++ b/lib/diamondtail/internallatencymanager.ex @@ -0,0 +1,21 @@ +defmodule Diamondtail.InternalLatencyManager do + use Agent + + def start_link(_) do + Agent.start_link(fn -> %{} end, name: __MODULE__) + end + + def best_depth(player_count, max_latency) do + Agent.get(__MODULE__, fn records -> + if Map.has_key?(records, player_count) do + # Find the highest depth value matching our max latency + # TODO + 3 + else + # We've never seen this player count before, we'll learn how fast we are on this player count after this is done + # For now we'll return 1 so we don't die + 1 + end + end) + end +end diff --git a/lib/diamondtail/population.ex b/lib/diamondtail/population.ex index 788d731..326c39f 100644 --- a/lib/diamondtail/population.ex +++ b/lib/diamondtail/population.ex @@ -25,7 +25,7 @@ defmodule Diamondtail.Population do field :food_distance_weight, float() field :own_accessible_tiles_weight, float() - field :search_depth, float() + field :latency_headroom, float() field :value_discount, float() end @@ -77,7 +77,7 @@ defmodule Diamondtail.Population do own_accessible_tiles_weight: random_between(0.7, 1), - search_depth: random_between(3.0, 5.0), + latency_headroom: random_between(20.0, 50.0), value_discount: random_between(1.0, 20.0) # as percentage } end diff --git a/lib/diamondtail/router.ex b/lib/diamondtail/router.ex index ef65ac0..5091a07 100644 --- a/lib/diamondtail/router.ex +++ b/lib/diamondtail/router.ex @@ -1,5 +1,6 @@ defmodule Diamondtail.Router do require Logger + alias Diamondtail.ExternalLatencyManager alias Diamondtail.Population alias Diamondtail.Brain alias Diamondtail.Genometracker @@ -38,11 +39,22 @@ defmodule Diamondtail.Router do post "/move" do game_id = conn.params["game"]["id"] player_id = conn.params["you"]["id"] + turn_number = conn.params["turn"] + + player_latency = conn.params["you"]["latency"] + if player_latency < conn.params["game"]["latency"] do + ExternalLatencyManager.report_total_latency({game_id, player_id, turn_number - 1}, player_latency) + end + genome = Genometracker.get_genome({game_id, player_id}) board = conn.params["board"] + game = conn.params["game"] self = conn.params["you"] - Logger.debug("Processing move #{game_id} as #{player_id} turn #{conn.params["turn"]}") - move = Brain.determine_move(genome, board, self) + Logger.debug("Processing move #{game_id} as #{player_id} turn #{turn_number}") + t_start = Time.utc_now() + move = Brain.determine_move(genome, board, self, game) + internal_latency = Time.diff(t_start, Time.utc_now(), :millisecond) + ExternalLatencyManager.report_internal_latency({game_id, player_id, turn_number}, internal_latency) Logger.debug("Our move: #{inspect(move)}") put_resp_content_type(conn, "application/json") |> send_resp(200, JSON.encode!(move)) @@ -51,8 +63,17 @@ defmodule Diamondtail.Router do post "/end" do game_id = conn.params["game"]["id"] player_id = conn.params["you"]["id"] + turn_number = conn.params["turn"] + + player_latency = conn.params["you"]["latency"] + if player_latency < conn.params["game"]["latency"] do + ExternalLatencyManager.report_total_latency({game_id, player_id, turn_number - 1}, player_latency) + end + # Don't leave any unresolved moves gathering dust in the latency manager + ExternalLatencyManager.clear_dangling_moves(fn {game, player, _} -> game == game_id && player == player_id end) + victory? = conn.params["board"]["snakes"] |> Enum.any?(fn %{"id" => snake_id} -> snake_id == player_id end) - Logger.info("Ending game '#{game_id}' as '#{player_id}' after #{conn.params["turn"]} turns. #{if victory?, do: "We won!", else: "We lost."}") + Logger.info("Ending game '#{game_id}' as '#{player_id}' after #{turn_number} turns. #{if victory?, do: "We won!", else: "We lost."}") Genometracker.game_end({game_id, player_id}, victory?) Logger.info("#{Population.size} genomes remain in the population.") Logger.debug("#{inspect(Population.avg)}") diff --git a/lib/operators.ex b/lib/operators.ex new file mode 100644 index 0000000..b5a117a --- /dev/null +++ b/lib/operators.ex @@ -0,0 +1,8 @@ +defmodule Diamondtail.Operators do + defmodule Implication do + @moduledoc """ + Implements logical implication, A ~> B is true if B is true or A is false + """ + def a ~> b, do: !a || b + end +end