Add a buncha shit
This commit is contained in:
parent
8412a2b954
commit
31e179a74a
@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
if [[ ! -d "/home/kodi/src/diamondtail" ]]; then
|
|
||||||
echo "Cannot find source directory; Did you move it?"
|
|
||||||
echo "(Looking for "/home/kodi/src/diamondtail")"
|
|
||||||
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# rebuild the cache forcefully
|
|
||||||
_nix_direnv_force_reload=1 direnv exec "/home/kodi/src/diamondtail" true
|
|
||||||
|
|
||||||
# Update the mtime for .envrc.
|
|
||||||
# This will cause direnv to reload again - but without re-building.
|
|
||||||
touch "/home/kodi/src/diamondtail/.envrc"
|
|
||||||
|
|
||||||
# Also update the timestamp of whatever profile_rc we have.
|
|
||||||
# This makes sure that we know we are up to date.
|
|
||||||
touch -r "/home/kodi/src/diamondtail/.envrc" "/home/kodi/src/diamondtail/.direnv"/*.rc
|
|
||||||
@ -1 +0,0 @@
|
|||||||
/nix/store/7nx4wv523ig8hgws8ihl3fchhxw1f9dv-source
|
|
||||||
@ -1 +0,0 @@
|
|||||||
/nix/store/ndig4f4dx8bmrmyr5vfm19g02r9l9ggm-source
|
|
||||||
@ -1 +0,0 @@
|
|||||||
/nix/store/panm5k6dglq8dixggfz8xf6y36ch9q61-source
|
|
||||||
@ -1 +0,0 @@
|
|||||||
/nix/store/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source
|
|
||||||
@ -1 +0,0 @@
|
|||||||
/nix/store/7ihccg0w57fj748mfin3scnb58f1z75x-nix-shell-env
|
|
||||||
File diff suppressed because it is too large
Load Diff
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,3 +23,4 @@ diamondtail-*.tar
|
|||||||
/tmp/
|
/tmp/
|
||||||
|
|
||||||
.elixir_ls
|
.elixir_ls
|
||||||
|
.direnv
|
||||||
37
.vscode/launch.json
vendored
Normal file
37
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "mix_task",
|
||||||
|
"name": "mix (Default task)",
|
||||||
|
"request": "launch",
|
||||||
|
"projectDir": "${workspaceRoot}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "mix_task",
|
||||||
|
"name": "mix test",
|
||||||
|
"request": "launch",
|
||||||
|
"task": "test",
|
||||||
|
"taskArgs": [
|
||||||
|
"--trace"
|
||||||
|
],
|
||||||
|
"startApps": true,
|
||||||
|
"projectDir": "${workspaceRoot}",
|
||||||
|
"requireFiles": [
|
||||||
|
"test/**/test_helper.exs",
|
||||||
|
"test/**/*_test.exs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "mix_task",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "mix run",
|
||||||
|
"task": "run",
|
||||||
|
"taskArgs": [ "--no-halt" ],
|
||||||
|
"projectDir": "${workspaceRoot}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
config/config.exs
Normal file
4
config/config.exs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import Config
|
||||||
|
|
||||||
|
config :logger,
|
||||||
|
level: :debug
|
||||||
@ -2,6 +2,8 @@ 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.Genometracker
|
||||||
|
alias Diamondtail.Population
|
||||||
|
|
||||||
use Application
|
use Application
|
||||||
|
|
||||||
@ -10,7 +12,10 @@ defmodule Diamondtail.Application do
|
|||||||
children = [
|
children = [
|
||||||
# Starts a worker by calling: Diamondtail.Worker.start_link(arg)
|
# Starts a worker by calling: Diamondtail.Worker.start_link(arg)
|
||||||
# {Diamondtail.Worker, arg}
|
# {Diamondtail.Worker, arg}
|
||||||
{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},
|
||||||
|
{Population, 1..10 |> Enum.map(fn _ -> Population.Genome.random end) |> Enum.to_list},
|
||||||
|
Genometracker
|
||||||
]
|
]
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
|||||||
221
lib/diamondtail/brain.ex
Normal file
221
lib/diamondtail/brain.ex
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
defmodule Diamondtail.Brain do
|
||||||
|
alias Diamondtail.Population.Genome
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
defmodule GameState do
|
||||||
|
use TypedStruct
|
||||||
|
|
||||||
|
defmodule Snake do
|
||||||
|
use TypedStruct
|
||||||
|
|
||||||
|
typedstruct enforce: true do
|
||||||
|
field :health, pos_integer()
|
||||||
|
field :body, [%{x: pos_integer(), y: pos_integer()}]
|
||||||
|
field :head, %{x: pos_integer(), y: pos_integer()}
|
||||||
|
field :length, pos_integer()
|
||||||
|
field :squad, String.t()
|
||||||
|
end
|
||||||
|
|
||||||
|
def from(snake) do
|
||||||
|
%Snake{
|
||||||
|
health: snake["health"],
|
||||||
|
body: snake["body"] |> Enum.map(fn %{"x" => x, "y" => y} -> %{x: x, y: y} end),
|
||||||
|
head: %{x: snake["head"]["x"], y: snake["head"]["y"]},
|
||||||
|
length: snake["length"],
|
||||||
|
squad: snake["squad"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
typedstruct enforce: true do
|
||||||
|
field :type, :q | :v
|
||||||
|
field :height, pos_integer()
|
||||||
|
field :width, pos_integer()
|
||||||
|
field :food, [%{x: pos_integer(), y: pos_integer()}]
|
||||||
|
field :hazards, [%{x: pos_integer(), y: pos_integer()}]
|
||||||
|
field :snakes, %{String.t() => Snake.t()}
|
||||||
|
field :self, String.t()
|
||||||
|
end
|
||||||
|
|
||||||
|
def from(board, self) do
|
||||||
|
%GameState{
|
||||||
|
type: :v,
|
||||||
|
height: board["height"],
|
||||||
|
width: board["width"],
|
||||||
|
food: board["food"] |> Enum.map(fn %{"x" => x, "y" => y} -> %{x: x, y: y} end),
|
||||||
|
hazards: board["hazards"] |> Enum.map(fn %{"x" => x, "y" => y} -> %{x: x, y: y} end),
|
||||||
|
snakes: board["snakes"] |> Enum.map(fn %{"id" => id} = snake -> {id, Snake.from(snake)} end) |> Map.new(),
|
||||||
|
self: self["id"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_actions(%GameState{type: :v} = state, move) do
|
||||||
|
# Apply the move to our snake
|
||||||
|
Map.put(state, :type, :q)
|
||||||
|
|> Map.update(:snakes, nil, fn snakes ->
|
||||||
|
Map.update(snakes, state.self, nil, &apply_action_to_snake(&1, move))
|
||||||
|
end)
|
||||||
|
# |> resolve_state()
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_actions(%GameState{type: :q} = state, moves) do
|
||||||
|
# Apply every move to the corresponding snake
|
||||||
|
Map.put(state, :type, :v)
|
||||||
|
|> Map.update(:snakes, nil, fn snakes ->
|
||||||
|
Enum.map(snakes, fn {id, s} ->
|
||||||
|
if Map.has_key?(moves, id) do
|
||||||
|
{id, apply_action_to_snake(s, moves[id])}
|
||||||
|
else
|
||||||
|
{id, s}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|> resolve_state()
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_actions(%GameState{type: :v, snakes: snakes, self: self}) do
|
||||||
|
if Map.has_key?(snakes, self) do
|
||||||
|
[:up, :down, :left, :right]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
|> Map.new()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def evaluate_state(genome, state, depth, alpha \\ Float.min_finite, beta \\ Float.max_finite)
|
||||||
|
def evaluate_state(genome, %GameState{type: :v} = state, 0, _, _) do
|
||||||
|
# If we're dead, the result is always super duper negative
|
||||||
|
if Map.has_key?(state.snakes, state.self) do
|
||||||
|
food = state.food
|
||||||
|
enemies = Enum.filter(state.snakes, fn {id, _} -> id != state.self end) |> Enum.map(fn {_, s} -> s end)
|
||||||
|
self = state.snakes[state.self]
|
||||||
|
|
||||||
|
Enum.count(enemies) * genome.enemy_alive_weight +
|
||||||
|
self.health * genome.own_health_weight +
|
||||||
|
Enum.sum_by(enemies, &(&1.health)) * genome.enemy_health_weight +
|
||||||
|
(Enum.map(enemies, &(abs(&1.head.x - self.head.x) + abs(&1.head.y - self.head.y))) |> Enum.min(&<=/2, fn -> 0 end)) * genome.enemy_head_distance_weight +
|
||||||
|
(Enum.map(food, &(abs(&1.x - self.head.x) + abs(&1.y - self.head.y))) |> Enum.min(&<=/2, fn -> 0 end)) * genome.food_distance_weight
|
||||||
|
else
|
||||||
|
Float.min_finite()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Don't do evaluations on Q states, since they are inherently incomplete
|
||||||
|
def evaluate_state(genome, %GameState{type: :q} = state, 0, alpha, beta) do
|
||||||
|
evaluate_state(genome, state, 1, alpha, beta)
|
||||||
|
end
|
||||||
|
|
||||||
|
def evaluate_state(genome, state, depth, alpha, beta) do
|
||||||
|
# In V states, we are in control, and we will choose the maximum value
|
||||||
|
# In Q states, opponents are in control, and they will choose the minimum value
|
||||||
|
op = if state.type == :v, do: &max/2, else: &min/2
|
||||||
|
init = if state.type == :v, do: Float.min_finite(), else: Float.max_finite()
|
||||||
|
comp = if state.type == :v, do: &(&1 >= beta), else: &(&1 <= alpha)
|
||||||
|
|
||||||
|
valid_actions = valid_actions(state)
|
||||||
|
if valid_actions == [] do
|
||||||
|
if Map.has_key?(state.snakes, state.self) do
|
||||||
|
Float.max_finite()
|
||||||
|
else
|
||||||
|
Float.min_finite()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{score, _alpha, _beta} = valid_actions(state)
|
||||||
|
|> Enum.reduce_while({init, alpha, beta}, fn action, {score, alpha, beta} ->
|
||||||
|
score = op.(score, evaluate_state(genome, GameState.apply_actions(state, action), depth - 1, alpha, beta))
|
||||||
|
if comp.(score) do
|
||||||
|
{:halt, {score, alpha, beta}}
|
||||||
|
else
|
||||||
|
if state.type == :v do
|
||||||
|
# Maximizing, update alpha
|
||||||
|
{:cont, {score, max(score, alpha), beta}}
|
||||||
|
else
|
||||||
|
# Minimizing, update beta
|
||||||
|
{:cont, {score, alpha, min(score, beta)}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Apply value discount
|
||||||
|
score * (1 - (genome.value_discount / 100))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp resolve_state(state) do
|
||||||
|
# Check which snakes have consumed food
|
||||||
|
Map.update(state, :snakes, nil, fn snakes ->
|
||||||
|
Enum.map(snakes, fn {id, %{head: head} = snake} ->
|
||||||
|
if Enum.member?(state.food, head) do
|
||||||
|
# Snake has gotten food, update it
|
||||||
|
{id, Map.put(snake, :health, 100)
|
||||||
|
|> Map.update(:body, nil, fn body -> [List.first(body) | body] end)
|
||||||
|
|> Map.update(:length, nil, &(&1 + 1))}
|
||||||
|
else
|
||||||
|
{id, snake}
|
||||||
|
end
|
||||||
|
end) |> Map.new()
|
||||||
|
end)
|
||||||
|
# Check which foods have been consumed
|
||||||
|
|> Map.update(:food, nil, fn food ->
|
||||||
|
Enum.filter(food, fn pos ->
|
||||||
|
# Careful, this is the state before being updated above
|
||||||
|
Enum.any?(state.snakes, fn {_, %{head: head}} -> head != pos end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
# Eliminate snakes
|
||||||
|
|> Map.update(:snakes, nil, fn snakes ->
|
||||||
|
Enum.filter(snakes, fn {snake_id, snake} ->
|
||||||
|
# Die if health is less than or equal to 0
|
||||||
|
snake.health > 0 &&
|
||||||
|
# Die if head is out of bounds
|
||||||
|
snake.head.x >= 0 && snake.head.x < state.width &&
|
||||||
|
snake.head.y >= 0 && snake.head.y < state.height &&
|
||||||
|
# Die if head is in a tail
|
||||||
|
!Enum.any?(state.snakes, fn {_, %{body: body}} -> Enum.member?(body, snake.head) end) &&
|
||||||
|
# Die if head is on head of a DIFFERENT snake of equal or greater length
|
||||||
|
!Enum.any?(state.snakes, fn {id, %{head: h, length: l}} -> snake.head == h && snake.length <= l && id != snake_id end)
|
||||||
|
end) |> Map.new()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_action_to_snake(snake, move) do
|
||||||
|
delta = case move do
|
||||||
|
:up -> %{x: 0, y: 1}
|
||||||
|
:down -> %{x: 0, y: -1}
|
||||||
|
:left -> %{x: -1, y: 0}
|
||||||
|
:right -> %{x: 1, y: 0}
|
||||||
|
end
|
||||||
|
|
||||||
|
Map.update(snake, :health, 0, &(&1 - 1))
|
||||||
|
|> Map.update(:body, nil, fn body -> List.delete_at(body, 0) ++ [snake.head] end)
|
||||||
|
|> Map.update(:head, nil, fn %{x: x, y: y} -> %{x: x + delta.x, y: y + delta.y} end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec determine_move(Genome.t(), map(), map()) :: %{move: :up | :down | :left | :right, shout: String.t()}
|
||||||
|
def determine_move(genome, board, self) do
|
||||||
|
base_state = GameState.from(board, self)
|
||||||
|
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))}
|
||||||
|
end, ordered: false)
|
||||||
|
|
||||||
|
{:ok, {action, score}} = Enum.max_by(options, fn {:ok, {_, s}} -> s end)
|
||||||
|
|
||||||
|
%{
|
||||||
|
move: action,
|
||||||
|
shout: "#{inspect(options |> Enum.map(fn {:ok, v} -> v end) |> Enum.to_list)} (#{score})"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
38
lib/diamondtail/genometracker.ex
Normal file
38
lib/diamondtail/genometracker.ex
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
defmodule Diamondtail.Genometracker do
|
||||||
|
@moduledoc """
|
||||||
|
This agent keeps track of which genome is associated with each running game,
|
||||||
|
it handles taking out a genome from the Population to assign it to a game and reintroducing new genomes after the end of the game.
|
||||||
|
"""
|
||||||
|
alias Diamondtail.Population.Genome
|
||||||
|
alias Diamondtail.Population
|
||||||
|
|
||||||
|
use Agent
|
||||||
|
|
||||||
|
def start_link(_) do
|
||||||
|
Agent.start_link(fn -> %{} end, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec game_start({String.t(), String.t()}) :: :ok
|
||||||
|
def game_start(id) do
|
||||||
|
genome = Population.take_random()
|
||||||
|
Agent.update(__MODULE__, &Map.put(&1, id, genome))
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_genome({String.t(), String.t()}) :: Genome.t() | nil
|
||||||
|
def get_genome(id) do
|
||||||
|
Agent.get(__MODULE__, &Map.get(&1, id))
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec game_end({String.t(), String.t()}, boolean()) :: nil
|
||||||
|
def game_end(id, victory?) do
|
||||||
|
genome = Agent.get_and_update(__MODULE__, &Map.pop(&1, id))
|
||||||
|
|
||||||
|
# If this genome lost, we will not reintroduce it into the population, effectively "killing" it
|
||||||
|
if victory? do
|
||||||
|
# The genome will create three "offspring", amount could be tweaked
|
||||||
|
for _ <- 0..3 do
|
||||||
|
Population.add(Genome.mutate(genome))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -1,5 +1,124 @@
|
|||||||
defmodule Diamondtail.Population do
|
defmodule Diamondtail.Population do
|
||||||
defmodule Genes do
|
@moduledoc """
|
||||||
|
This module defines an Agent which maintains a list of every individual in the population, allowing an individual to be taken out or introduced.
|
||||||
|
"""
|
||||||
|
use Agent
|
||||||
|
|
||||||
|
defmodule Genome do
|
||||||
|
@moduledoc """
|
||||||
|
Genomes represents the parameters of an individual, which tweaks the weights in the algorithm and implements the genetic algorithm "learning" functionality
|
||||||
|
"""
|
||||||
|
use TypedStruct
|
||||||
|
|
||||||
|
typedstruct enforce: true do
|
||||||
|
field :mutation_chance, float()
|
||||||
|
field :mutation_max_amplitude, float()
|
||||||
|
|
||||||
|
field :enemy_alive_weight, float()
|
||||||
|
field :own_health_weight, float()
|
||||||
|
field :enemy_health_weight, float()
|
||||||
|
field :enemy_head_distance_weight, float()
|
||||||
|
field :food_distance_weight, float()
|
||||||
|
|
||||||
|
field :search_depth, float()
|
||||||
|
field :value_discount, float()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp random_between(bot, top) do
|
||||||
|
bot + :rand.uniform() * (top - bot)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec mutate(Genome.t()) :: Genome.t()
|
||||||
|
def mutate(genome) do
|
||||||
|
Map.from_struct(genome)
|
||||||
|
|> Enum.map(fn {key, value} ->
|
||||||
|
if :rand.uniform() <= genome.mutation_chance do
|
||||||
|
{key, value + random_between(-genome.mutation_max_amplitude, genome.mutation_max_amplitude)}
|
||||||
|
else
|
||||||
|
{key, value}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Map.new() |> Map.put(:__struct__, Genome)
|
||||||
|
end
|
||||||
|
|
||||||
|
def average(genomes) do
|
||||||
|
Enum.reduce(genomes, zero(), fn g, acc ->
|
||||||
|
Map.from_struct(acc)
|
||||||
|
|> Enum.map(fn {k, v} ->
|
||||||
|
{k, v + get_in(g, [Access.key!(k)])}
|
||||||
|
end)
|
||||||
|
|> Map.new() |> Map.put(:__struct__, Genome)
|
||||||
|
end) |> Map.from_struct()
|
||||||
|
|> Enum.map(fn {k, v} ->
|
||||||
|
{k, v / length(genomes)}
|
||||||
|
end) |> Map.new() |> Map.put(:__struct__, Genome)
|
||||||
|
end
|
||||||
|
|
||||||
|
def random() do
|
||||||
|
%Genome{
|
||||||
|
mutation_chance: random_between(0.1, 0.2),
|
||||||
|
mutation_max_amplitude: random_between(0.1, 0.05),
|
||||||
|
enemy_alive_weight: random_between(-100.0, -200.0),
|
||||||
|
own_health_weight: random_between(0.7, 1.0),
|
||||||
|
enemy_health_weight: random_between(-0.8, -0.4),
|
||||||
|
|
||||||
|
# both of the following are compared to the nearest object, to avoid overrewarding when there are more enemies/foods
|
||||||
|
enemy_head_distance_weight: random_between(1, 2), # maximize distance, reward staying away
|
||||||
|
food_distance_weight: random_between(-2, -1), # minimize distance, reward staying near
|
||||||
|
|
||||||
|
search_depth: random_between(3.0, 7.0),
|
||||||
|
value_discount: random_between(1.0, 10.0) # as percentage
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Not very computationally efficient but avoids repeating code
|
||||||
|
def zero() do
|
||||||
|
Map.from_struct(random())
|
||||||
|
|> Enum.map(fn {key, _value} ->
|
||||||
|
{key, 0}
|
||||||
|
end)
|
||||||
|
|> Map.new() |> Map.put(:__struct__, Genome)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_link(initial_value) do
|
||||||
|
Agent.start_link(fn -> initial_value end, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Takes out and returns a random genome from the population. If the population is empty, a new random genome will be introduced.
|
||||||
|
"""
|
||||||
|
@spec take_random() :: Genome.t()
|
||||||
|
def take_random() do
|
||||||
|
Agent.get_and_update(__MODULE__, fn list ->
|
||||||
|
index = :rand.uniform(length(list)) - 1
|
||||||
|
{got, list} = List.pop_at(list, index)
|
||||||
|
list = if list == [], do: [Genome.random], else: list
|
||||||
|
{got, list}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Adds a genome to the population
|
||||||
|
"""
|
||||||
|
@spec add(Genome.t()) :: :ok
|
||||||
|
def add(genome) do
|
||||||
|
Agent.update(__MODULE__, &[genome | &1])
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the amount of genomes in the population
|
||||||
|
"""
|
||||||
|
@spec size() :: integer()
|
||||||
|
def size() do
|
||||||
|
Agent.get(__MODULE__, &length/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the "average" genome, to get a sense of the population's tendencies
|
||||||
|
"""
|
||||||
|
@spec avg() :: Genome.t()
|
||||||
|
def avg() do
|
||||||
|
Agent.get(__MODULE__, &Genome.average/1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,16 +1,21 @@
|
|||||||
defmodule Diamondtail.Router do
|
defmodule Diamondtail.Router do
|
||||||
|
require Logger
|
||||||
|
alias Diamondtail.Population
|
||||||
|
alias Diamondtail.Brain
|
||||||
|
alias Diamondtail.Genometracker
|
||||||
use Plug.Router
|
use Plug.Router
|
||||||
|
|
||||||
|
plug Plug.Parsers,
|
||||||
|
parsers: [:json],
|
||||||
|
json_decoder: Jason
|
||||||
|
|
||||||
plug Plug.Logger
|
plug Plug.Logger
|
||||||
plug :match
|
plug :match
|
||||||
plug :dispatch
|
plug :dispatch
|
||||||
|
|
||||||
plug Plug.Parsers,
|
|
||||||
parsers: [:json],
|
|
||||||
json_decoder: JSON
|
|
||||||
|
|
||||||
get "/" do
|
get "/" do
|
||||||
send_resp(conn, 200, JSON.encode!(%{
|
put_resp_content_type(conn, "application/json")
|
||||||
|
|> send_resp(200, JSON.encode!(%{
|
||||||
apiversion: "1",
|
apiversion: "1",
|
||||||
author: "kodicraft",
|
author: "kodicraft",
|
||||||
color: "#8cd9ff",
|
color: "#8cd9ff",
|
||||||
@ -19,4 +24,39 @@ defmodule Diamondtail.Router do
|
|||||||
version: Mix.Project.config()[:version]
|
version: Mix.Project.config()[:version]
|
||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
post "/start" do
|
||||||
|
# Assign a genome to this game
|
||||||
|
game_id = conn.params["game"]["id"]
|
||||||
|
player_id = conn.params["you"]["id"]
|
||||||
|
Genometracker.game_start({game_id, player_id})
|
||||||
|
Logger.info("Starting new game '#{game_id}' as '#{player_id}'")
|
||||||
|
put_resp_content_type(conn, "application/json")
|
||||||
|
|> send_resp(200, JSON.encode!(%{status: "success"}))
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/move" do
|
||||||
|
game_id = conn.params["game"]["id"]
|
||||||
|
player_id = conn.params["you"]["id"]
|
||||||
|
genome = Genometracker.get_genome({game_id, player_id})
|
||||||
|
board = conn.params["board"]
|
||||||
|
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("Our move: #{inspect(move)}")
|
||||||
|
put_resp_content_type(conn, "application/json")
|
||||||
|
|> send_resp(200, JSON.encode!(move))
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/end" do
|
||||||
|
game_id = conn.params["game"]["id"]
|
||||||
|
player_id = conn.params["you"]["id"]
|
||||||
|
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."}")
|
||||||
|
Genometracker.game_end({game_id, player_id}, victory?)
|
||||||
|
Logger.info("#{Population.size} genomes remain in the population.")
|
||||||
|
Logger.debug("#{inspect(Population.avg)}")
|
||||||
|
put_resp_content_type(conn, "application/json")
|
||||||
|
|> send_resp(200, JSON.encode!(%{thank: "you"}))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
1
mix.exs
1
mix.exs
@ -26,6 +26,7 @@ defmodule Diamondtail.MixProject do
|
|||||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||||
{:credo, "~> 1.7"},
|
{:credo, "~> 1.7"},
|
||||||
{:plug_cowboy, "~> 2.0"},
|
{:plug_cowboy, "~> 2.0"},
|
||||||
|
{:typed_struct, "~> 0.3.0"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
1
mix.lock
1
mix.lock
@ -12,4 +12,5 @@
|
|||||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||||
|
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user