Begin implementing LAID
This commit is contained in:
parent
c1fbe9cd80
commit
ea897b0292
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
33
lib/diamondtail/externallatencymanager.ex
Normal file
33
lib/diamondtail/externallatencymanager.ex
Normal 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
|
||||
21
lib/diamondtail/internallatencymanager.ex
Normal file
21
lib/diamondtail/internallatencymanager.ex
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
8
lib/operators.ex
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user