Begin implementing LAID

This commit is contained in:
Kodi Craft 2025-11-10 19:26:59 +01:00
parent c1fbe9cd80
commit ea897b0292
Signed by: kodi
GPG Key ID: 69D9EED60B242822
7 changed files with 104 additions and 15 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.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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)}")

8
lib/operators.ex Normal file
View File

@ -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