Finish implementing LAID

This commit is contained in:
Kodi Craft 2025-11-10 23:05:20 +01:00
parent ea897b0292
commit 23b507dc4c
Signed by: kodi
GPG Key ID: 69D9EED60B242822
6 changed files with 49 additions and 11 deletions

View File

@ -2,6 +2,7 @@ defmodule Diamondtail.Application do
# See https://hexdocs.pm/elixir/Application.html # See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications # for more information on OTP Applications
@moduledoc false @moduledoc false
alias Diamondtail.InternalLatencyManager
alias Diamondtail.ExternalLatencyManager alias Diamondtail.ExternalLatencyManager
alias Diamondtail.Genometracker alias Diamondtail.Genometracker
alias Diamondtail.Population alias Diamondtail.Population
@ -16,7 +17,7 @@ defmodule Diamondtail.Application do
{Plug.Cowboy, scheme: :http, plug: Diamondtail.Router, options: [port: 4001]}, {Plug.Cowboy, scheme: :http, plug: Diamondtail.Router, options: [port: 4001]},
{Task.Supervisor, name: Diamondtail.TaskSupervisor}, {Task.Supervisor, name: Diamondtail.TaskSupervisor},
{Population, 1..10 |> Enum.map(fn _ -> Population.Genome.random end) |> Enum.to_list}, {Population, 1..10 |> Enum.map(fn _ -> Population.Genome.random end) |> Enum.to_list},
Genometracker, ExternalLatencyManager Genometracker, ExternalLatencyManager, InternalLatencyManager
] ]
# See https://hexdocs.pm/elixir/Supervisor.html # See https://hexdocs.pm/elixir/Supervisor.html

View File

@ -1,5 +1,4 @@
defmodule Diamondtail.Brain do defmodule Diamondtail.Brain do
import Diamondtail.Operators.Implication
alias Diamondtail.InternalLatencyManager alias Diamondtail.InternalLatencyManager
alias Diamondtail.ExternalLatencyManager alias Diamondtail.ExternalLatencyManager
alias Diamondtail.Population.Genome alias Diamondtail.Population.Genome
@ -242,26 +241,32 @@ defmodule Diamondtail.Brain do
@spec determine_move(Genome.t(), map(), map(), map()) :: %{move: :up | :down | :left | :right, shout: String.t()} @spec determine_move(Genome.t(), map(), map(), map()) :: %{move: :up | :down | :left | :right, shout: String.t()}
def determine_move(genome, board, self, game) do def determine_move(genome, board, self, game) do
start_time = Time.utc_now()
base_state = GameState.from(board, self) base_state = GameState.from(board, self)
external_latency = ExternalLatencyManager.get_external_latency() external_latency = ExternalLatencyManager.get_external_latency()
max_latency = game["timeout"] max_latency = game["timeout"]
target_latency = max_latency - external_latency - genome.latency_headroom target_latency = max_latency - external_latency - genome.latency_headroom
depth = InternalLatencyManager.best_depth(map_size(base_state.snakes), target_latency) depth = InternalLatencyManager.best_depth(map_size(base_state.snakes), target_latency)
Logger.debug("Using a depth of #{depth}")
options = GameState.valid_actions(base_state) |> options = GameState.valid_actions(base_state) |>
Task.async_stream(fn action -> Task.async_stream(fn action ->
qstate = GameState.apply_actions(base_state, action) qstate = GameState.apply_actions(base_state, action)
{action, GameState.evaluate_state(genome, qstate, depth)} {action, GameState.evaluate_state(genome, qstate, depth)}
end, ordered: false) end, ordered: false, timeout: :infinity)
if Enum.empty?(options) do if Enum.empty?(options) do
end_time = Time.utc_now()
InternalLatencyManager.register_latency(map_size(base_state.snakes), depth, Time.diff(end_time, start_time, :millisecond))
%{ %{
move: :left, move: :left,
shout: "I'm dead, good game!" shout: "I'm dead, good game!"
} }
else else
{:ok, {action, score}} = Enum.max_by(options, fn {:ok, {_, s}} -> s end) {:ok, {action, score}} = Enum.max_by(options, fn {:ok, {_, s}} -> s end)
end_time = Time.utc_now()
InternalLatencyManager.register_latency(map_size(base_state.snakes), depth, Time.diff(end_time, start_time, :millisecond))
%{ %{
move: action, move: action,

View File

@ -22,12 +22,17 @@ defmodule Diamondtail.ExternalLatencyManager do
def get_external_latency() do def get_external_latency() do
{tot, dp} = Agent.get(__MODULE__, fn {tot, dp, _} -> {tot, dp} end) {tot, dp} = Agent.get(__MODULE__, fn {tot, dp, _} -> {tot, dp} end)
tot / dp if dp == 0 do
# Some guesstimate
100
else
tot / dp
end
end end
def clear_dangling_moves(predicate \\ fn -> true end) do def clear_dangling_moves(predicate \\ fn -> true end) do
Agent.update(__MODULE__, fn {tot_latency, tot_datapoints, dangling_moves} -> Agent.update(__MODULE__, fn {tot_latency, tot_datapoints, dangling_moves} ->
{tot_latency, tot_datapoints, Map.filter(dangling_moves, fn {k, _} -> !predicate.(k) end)} {tot_latency, tot_datapoints, Map.reject(dangling_moves, fn {k, _} -> predicate.(k) end)}
end) end)
end end
end end

View File

@ -1,21 +1,46 @@
defmodule Diamondtail.InternalLatencyManager do defmodule Diamondtail.InternalLatencyManager do
use Agent use Agent
require Logger
def start_link(_) do def start_link(_) do
Agent.start_link(fn -> %{} end, name: __MODULE__) Agent.start_link(fn -> %{} end, name: __MODULE__)
end end
def best_depth(player_count, max_latency) do def best_depth(player_count, max_latency) do
Agent.get(__MODULE__, fn records -> Logger.debug("Looking for the best depth for #{player_count} players within #{max_latency}ms")
if Map.has_key?(records, player_count) do records = Agent.get(__MODULE__, &(&1))
if Map.has_key?(records, player_count) do
# Find the highest depth value matching our max latency # Find the highest depth value matching our max latency
# TODO Enum.reduce_while(records[player_count], 1, fn {tot, reg}, acc ->
3 # TODO: "learning"
if (tot / reg) > max_latency do
{:halt, acc - 2}
else
{:cont, acc + 2}
end
end)
else else
# We've never seen this player count before, we'll learn how fast we are on this player count after this is done # 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 # For now we'll return 1 so we don't die
1 1
end end
end
def register_latency(player_count, depth, internal_latency) do
Logger.debug("Registering that a search of depth #{depth} with #{player_count} players took #{internal_latency}ms")
Agent.update(__MODULE__, fn records ->
Map.update(records, player_count, [], fn list ->
index = floor((depth - 1) / 2) # Floor should be avoidable but we do it anyway to be sure
if index >= length(list) do
list ++ [{internal_latency, 1}]
else
List.update_at(list, index, fn {tot, reg} -> {tot + internal_latency, reg + 1} end)
end
end)
end) end)
end end
def get_latencies(player_count) do
Agent.get(__MODULE__, &(&1[player_count]))
end
end end

View File

@ -77,7 +77,7 @@ defmodule Diamondtail.Population do
own_accessible_tiles_weight: random_between(0.7, 1), own_accessible_tiles_weight: random_between(0.7, 1),
latency_headroom: random_between(20.0, 50.0), latency_headroom: random_between(80.0, 100.0),
value_discount: random_between(1.0, 20.0) # as percentage value_discount: random_between(1.0, 20.0) # as percentage
} }
end end

View File

@ -52,7 +52,9 @@ defmodule Diamondtail.Router do
self = conn.params["you"] self = conn.params["you"]
Logger.debug("Processing move #{game_id} as #{player_id} turn #{turn_number}") Logger.debug("Processing move #{game_id} as #{player_id} turn #{turn_number}")
t_start = Time.utc_now() t_start = Time.utc_now()
move = Brain.determine_move(genome, board, self, game) # nolink so internal latency is still reported
move_task = Task.Supervisor.async_nolink(Diamondtail.TaskSupervisor, fn -> Brain.determine_move(genome, board, self, game) end)
move = Task.await(move_task)
internal_latency = Time.diff(t_start, Time.utc_now(), :millisecond) internal_latency = Time.diff(t_start, Time.utc_now(), :millisecond)
ExternalLatencyManager.report_internal_latency({game_id, player_id, turn_number}, internal_latency) ExternalLatencyManager.report_internal_latency({game_id, player_id, turn_number}, internal_latency)
Logger.debug("Our move: #{inspect(move)}") Logger.debug("Our move: #{inspect(move)}")