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
# for more information on OTP Applications
@moduledoc false
alias Diamondtail.InternalLatencyManager
alias Diamondtail.ExternalLatencyManager
alias Diamondtail.Genometracker
alias Diamondtail.Population
@ -16,7 +17,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, ExternalLatencyManager
Genometracker, ExternalLatencyManager, InternalLatencyManager
]
# See https://hexdocs.pm/elixir/Supervisor.html

View File

@ -1,5 +1,4 @@
defmodule Diamondtail.Brain do
import Diamondtail.Operators.Implication
alias Diamondtail.InternalLatencyManager
alias Diamondtail.ExternalLatencyManager
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()}
def determine_move(genome, board, self, game) do
start_time = Time.utc_now()
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)
Logger.debug("Using a depth of #{depth}")
options = GameState.valid_actions(base_state) |>
Task.async_stream(fn action ->
qstate = GameState.apply_actions(base_state, action)
{action, GameState.evaluate_state(genome, qstate, depth)}
end, ordered: false)
end, ordered: false, timeout: :infinity)
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,
shout: "I'm dead, good game!"
}
else
{: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,

View File

@ -22,12 +22,17 @@ defmodule Diamondtail.ExternalLatencyManager do
def get_external_latency() do
{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
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)}
{tot_latency, tot_datapoints, Map.reject(dangling_moves, fn {k, _} -> predicate.(k) end)}
end)
end
end

View File

@ -1,21 +1,46 @@
defmodule Diamondtail.InternalLatencyManager do
use Agent
require Logger
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
Logger.debug("Looking for the best depth for #{player_count} players within #{max_latency}ms")
records = Agent.get(__MODULE__, &(&1))
if Map.has_key?(records, player_count) do
# Find the highest depth value matching our max latency
# TODO
3
Enum.reduce_while(records[player_count], 1, fn {tot, reg}, acc ->
# TODO: "learning"
if (tot / reg) > max_latency do
{:halt, acc - 2}
else
{:cont, acc + 2}
end
end)
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
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
def get_latencies(player_count) do
Agent.get(__MODULE__, &(&1[player_count]))
end
end

View File

@ -77,7 +77,7 @@ defmodule Diamondtail.Population do
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
}
end

View File

@ -52,7 +52,9 @@ defmodule Diamondtail.Router do
self = conn.params["you"]
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)
# 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)
ExternalLatencyManager.report_internal_latency({game_id, player_id, turn_number}, internal_latency)
Logger.debug("Our move: #{inspect(move)}")