Finish up Plugin.Supervisor, replace markov and wordbot implementations with it
Both markov and wordbot have some auxiliary processes that run to keep track of things. Previously, they both had custom supervisors grafted on to the Plugin.Base - now, this grafting is automated with Plugin.Supervisor. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
defmodule Omnibot.Contrib.Markov.Bot do
|
||||
use Omnibot.Plugin
|
||||
alias Omnibot.Contrib.Markov.Chain
|
||||
|
||||
@impl true
|
||||
def on_init(_cfg) do
|
||||
# Create the markov database
|
||||
path = String.to_atom(cfg()[:path])
|
||||
{:ok, db} = :dets.open_file(path, [:named_table])
|
||||
chains = :ets.new(:markov_chains, [:public])
|
||||
:dets.to_ets(db, chains)
|
||||
:dets.close(db)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_channel_msg(_irc, channel, nick, msg) do
|
||||
train(channel, nick, msg)
|
||||
end
|
||||
|
||||
def train(channel, user, msg) do
|
||||
chain = (user_chain(channel, user) || create_user_chain(channel, user))
|
||||
|> Chain.train(msg)
|
||||
true = update_user_chain(channel, user, chain)
|
||||
end
|
||||
|
||||
def user_chain(channel, user) do
|
||||
db = state()
|
||||
case :ets.lookup(db, {channel, user}) do
|
||||
[] -> nil
|
||||
[{{^channel, ^user}, chains}] -> chains
|
||||
end
|
||||
end
|
||||
|
||||
def update_user_chain(channel, user, chain) do
|
||||
db = state()
|
||||
case user_chain(channel, user) do
|
||||
nil -> :ets.insert_new(db, {{channel, user}, chain})
|
||||
chain -> :ets.insert(db, {{channel, user}, chain})
|
||||
end
|
||||
end
|
||||
|
||||
defp create_user_chain(channel, user) do
|
||||
true = update_user_chain(channel, user, %Chain{order: cfg()[:order]})
|
||||
user_chain(channel, user)
|
||||
end
|
||||
|
||||
def save_chains() do
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,32 +1,64 @@
|
||||
defmodule Omnibot.Contrib.Markov do
|
||||
use Omnibot.Plugin.Base
|
||||
alias Omnibot.Contrib.Markov
|
||||
use Supervisor
|
||||
use Omnibot.Plugin.Supervisor
|
||||
alias Omnibot.Contrib.Markov.Chain
|
||||
require Logger
|
||||
|
||||
@default_config path: "markov", order: 2, save_every: 5 * 60
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts[:cfg], opts)
|
||||
@impl true
|
||||
def children(_cfg, _state) do
|
||||
[{Task, fn ->
|
||||
Stream.timer(1000)
|
||||
|> Stream.cycle()
|
||||
|> Stream.each(fn _ -> save_chains() end)
|
||||
|> Stream.run()
|
||||
end}]
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(cfg) do
|
||||
children = [
|
||||
{Markov.Bot, cfg: cfg, name: Omnibot.Contrib.Markov.Bot},
|
||||
{Task, fn -> save_loop(cfg) end}
|
||||
]
|
||||
Supervisor.init(children, strategy: :one_for_all)
|
||||
end
|
||||
|
||||
defp save_loop(cfg) do
|
||||
save_every = cfg[:save_every]
|
||||
Process.sleep(save_every * 1000)
|
||||
Markov.Bot.save_chains()
|
||||
def on_init(_cfg) do
|
||||
# Create the markov database
|
||||
path = String.to_atom(cfg()[:path])
|
||||
{:ok, db} = :dets.open_file(path, [:named_table])
|
||||
chains = :ets.new(:markov_chains, [:public])
|
||||
:dets.to_ets(db, chains)
|
||||
:dets.close(db)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_msg(irc, msg), do: Markov.Bot.on_msg(irc, msg)
|
||||
def on_channel_msg(_irc, channel, nick, msg) do
|
||||
train(channel, nick, msg)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_channel_msg(irc, channel, nick, msg), do: Markov.Bot.on_channel_msg(irc, channel, nick, msg)
|
||||
def train(channel, user, msg) do
|
||||
chain = (user_chain(channel, user) || create_user_chain(channel, user))
|
||||
|> Chain.train(msg)
|
||||
true = update_user_chain(channel, user, chain)
|
||||
end
|
||||
|
||||
def user_chain(channel, user) do
|
||||
db = state()
|
||||
case :ets.lookup(db, {channel, user}) do
|
||||
[] -> nil
|
||||
[{{^channel, ^user}, chains}] -> chains
|
||||
end
|
||||
end
|
||||
|
||||
def update_user_chain(channel, user, chain) do
|
||||
db = state()
|
||||
case user_chain(channel, user) do
|
||||
nil -> :ets.insert_new(db, {{channel, user}, chain})
|
||||
chain -> :ets.insert(db, {{channel, user}, chain})
|
||||
end
|
||||
end
|
||||
|
||||
defp create_user_chain(channel, user) do
|
||||
true = update_user_chain(channel, user, %Chain{order: cfg()[:order]})
|
||||
user_chain(channel, user)
|
||||
end
|
||||
|
||||
def save_chains() do
|
||||
# TODO
|
||||
Logger.info("Saved markov chains")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
defmodule Omnibot.Contrib.Wordbot.Bot do
|
||||
use Omnibot.Plugin
|
||||
|
||||
alias Omnibot.{Contrib.Wordbot, Irc, State, Util}
|
||||
require Logger
|
||||
|
||||
@split_pattern ~r/[\s\b]+/
|
||||
|
||||
## Bot commands
|
||||
|
||||
command "!wordbot", ["leaderboard"] do
|
||||
Wordbot.Db.leaderboard(channel)
|
||||
|> Enum.sort_by(& &1.score)
|
||||
|> Enum.reverse()
|
||||
|> Enum.take(5)
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn {%{user: nick, score: score}, rank} -> "#{rank + 1}. #{Util.denotify_nick(nick)}. #{score}" end)
|
||||
|> Enum.each(&Irc.send_to(irc, channel, &1))
|
||||
end
|
||||
|
||||
## Client API
|
||||
|
||||
def words() do
|
||||
{words, _watchers} = state()
|
||||
words
|
||||
end
|
||||
|
||||
defp watchers() do
|
||||
{_words, watchers} = state()
|
||||
watchers
|
||||
end
|
||||
|
||||
defp update_watchers(mapping) do
|
||||
update_state(fn {words, watchers} -> {words, apply(mapping, [watchers])} end)
|
||||
end
|
||||
|
||||
defp add_watcher(channel, task) do
|
||||
update_watchers(&Map.put(&1, channel, task))
|
||||
end
|
||||
|
||||
defp delete_watcher(channel) do
|
||||
task = watchers()[channel]
|
||||
update_watchers(&Map.delete(&1, channel))
|
||||
task
|
||||
end
|
||||
|
||||
defp lookup_watcher(channel) do
|
||||
Map.get(watchers(), channel)
|
||||
end
|
||||
|
||||
defp has_watcher?(channel) do
|
||||
case lookup_watcher(channel) do
|
||||
nil -> false
|
||||
task -> Process.alive?(task)
|
||||
end
|
||||
end
|
||||
|
||||
def start_round(irc, channel) do
|
||||
# Get round config
|
||||
cfg = cfg()
|
||||
num_words = cfg[:words_per_round]
|
||||
duration = cfg[:hours_per_round] * 3600
|
||||
# Select words
|
||||
words = Enum.take_random(words(), num_words)
|
||||
# Try to start the round - if it's already running then that's OK
|
||||
case Wordbot.Db.start_round(channel, words, duration) do
|
||||
:ok -> Logger.debug("Started new wordbot round for #{channel}")
|
||||
{:error, :game_running} -> Logger.debug("Wordbot game already running for #{channel}")
|
||||
end
|
||||
|
||||
# Try to start a watcher if there isn't one running
|
||||
if !has_watcher?(channel),
|
||||
do: start_watcher(irc, channel)
|
||||
end
|
||||
|
||||
defp start_watcher(irc, channel) do
|
||||
# Start a watcher for the given channel
|
||||
Logger.debug("Starting wordbot game watcher for #{channel}")
|
||||
# Assert that there isn't a running watcher for the current channel
|
||||
false = has_watcher?(channel)
|
||||
task = Task.Supervisor.async_nolink(
|
||||
Wordbot.Watchers,
|
||||
fn -> watch_game(irc, channel) end,
|
||||
[shutdown: :brutal_kill]
|
||||
)
|
||||
add_watcher(channel, task)
|
||||
end
|
||||
|
||||
defp watch_game(irc, channel) do
|
||||
# Poll every second to check if a game is finished
|
||||
if Wordbot.Db.game_active?(channel) do
|
||||
Process.sleep(1000)
|
||||
watch_game(irc, channel)
|
||||
else
|
||||
finish_round(irc, channel)
|
||||
end
|
||||
end
|
||||
|
||||
def finish_round(irc, channel) do
|
||||
Logger.debug("Finishing wordbot round for #{channel}")
|
||||
|
||||
# Announce scores
|
||||
Irc.send_to(irc, channel, "Game over. Here were the scores:")
|
||||
scores = Wordbot.Db.scores(channel)
|
||||
|> Enum.sort_by(&(&1.score))
|
||||
|> Enum.reverse()
|
||||
|
||||
# Ranking is a little weird because we want to rank people so that having
|
||||
# the same score will give the same ranking, e.g.
|
||||
# 1. user1. 4
|
||||
# 2. user2. 3
|
||||
# 2. user3. 3
|
||||
# 3. user4. 1
|
||||
rankings = scores
|
||||
|> Enum.map(&(&1.score))
|
||||
|> Enum.sort()
|
||||
|> Enum.uniq()
|
||||
|> Enum.reverse()
|
||||
|> Enum.with_index()
|
||||
|> Map.new()
|
||||
|> IO.inspect()
|
||||
|
||||
Enum.each(scores, &Irc.send_to(irc, channel, "#{rankings[&1.score] + 1}. #{Util.denotify_nick(&1.user)}. #{&1.score}"))
|
||||
|
||||
# Stop the watcher, start new round
|
||||
delete_watcher(channel)
|
||||
start_round(irc, channel)
|
||||
end
|
||||
|
||||
## Plugin callbacks
|
||||
|
||||
@impl true
|
||||
def on_init(cfg) do
|
||||
Wordbot.Db.ensure_db()
|
||||
words = File.read!(cfg[:wordbot_source])
|
||||
|> String.split("\n")
|
||||
watchers = %{}
|
||||
{words, watchers}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_channel_msg(irc, channel, nick, msg) do
|
||||
words = Regex.split(@split_pattern, msg) |> MapSet.new()
|
||||
game_words = Wordbot.Db.unmatched_words(channel) |> MapSet.new()
|
||||
MapSet.intersection(words, game_words)
|
||||
|> Enum.each(fn word ->
|
||||
Wordbot.Db.add_score(channel, nick, word, msg)
|
||||
Irc.send_to(irc, channel, "#{nick}: Congrats! '#{word}' is good for 1 point.")
|
||||
end)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_join(irc, channel, who) do
|
||||
# Attempt to start a new round
|
||||
if State.cfg().nick == who do
|
||||
start_round(irc, channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,28 +1,183 @@
|
||||
defmodule Omnibot.Contrib.Wordbot do
|
||||
use Omnibot.Plugin.Base
|
||||
use Supervisor
|
||||
use Omnibot.Plugin.Supervisor
|
||||
alias Omnibot.{Contrib.Wordbot, State, Util}
|
||||
require Logger
|
||||
|
||||
alias Omnibot.Contrib.Wordbot
|
||||
|
||||
@default_config wordbot_source: "words.txt", wordbot_db: "wordbot.db", words_per_round: 300, hours_per_round: 5
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts[:cfg], opts)
|
||||
#def start_link(opts) do
|
||||
#Supervisor.start_link(__MODULE__, opts[:cfg], opts)
|
||||
#end
|
||||
|
||||
#@impl true
|
||||
#def init(cfg) do
|
||||
# children = [
|
||||
# {Task.Supervisor, name: Omnibot.Contrib.Wordbot.Watchers, strategy: :one_for_one},
|
||||
# Wordbot.Db.child_spec(cfg[:wordbot_db]),
|
||||
# {Wordbot.Bot, cfg: cfg, name: Omnibot.Contrib.Wordbot.Bot},
|
||||
# ]
|
||||
|
||||
# Supervisor.init(children, strategy: :one_for_all)
|
||||
#end
|
||||
|
||||
@impl true
|
||||
def children(cfg, _state) do
|
||||
[
|
||||
{Task.Supervisor, name: Omnibot.Contrib.Wordbot.Watchers, strategy: :one_for_one},
|
||||
Wordbot.Db.child_spec(cfg[:wordbot_db]),
|
||||
]
|
||||
end
|
||||
|
||||
@split_pattern ~r/[\s\b]+/
|
||||
|
||||
## Bot commands
|
||||
|
||||
command "!wordbot", ["leaderboard"] do
|
||||
Wordbot.Db.leaderboard(channel)
|
||||
|> Enum.sort_by(& &1.score)
|
||||
|> Enum.reverse()
|
||||
|> Enum.take(5)
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn {%{user: nick, score: score}, rank} -> "#{rank + 1}. #{Util.denotify_nick(nick)}. #{score}" end)
|
||||
|> Enum.each(&Irc.send_to(irc, channel, &1))
|
||||
end
|
||||
|
||||
## Client API
|
||||
|
||||
def words() do
|
||||
{words, _watchers} = state()
|
||||
words
|
||||
end
|
||||
|
||||
defp watchers() do
|
||||
{_words, watchers} = state()
|
||||
watchers
|
||||
end
|
||||
|
||||
defp update_watchers(mapping) do
|
||||
update_state(fn {words, watchers} -> {words, apply(mapping, [watchers])} end)
|
||||
end
|
||||
|
||||
defp add_watcher(channel, task) do
|
||||
update_watchers(&Map.put(&1, channel, task))
|
||||
end
|
||||
|
||||
defp delete_watcher(channel) do
|
||||
task = watchers()[channel]
|
||||
update_watchers(&Map.delete(&1, channel))
|
||||
task
|
||||
end
|
||||
|
||||
defp lookup_watcher(channel) do
|
||||
Map.get(watchers(), channel)
|
||||
end
|
||||
|
||||
defp has_watcher?(channel) do
|
||||
case lookup_watcher(channel) do
|
||||
nil -> false
|
||||
task -> Process.alive?(task)
|
||||
end
|
||||
end
|
||||
|
||||
def start_round(irc, channel) do
|
||||
# Get round config
|
||||
cfg = cfg()
|
||||
num_words = cfg[:words_per_round]
|
||||
duration = cfg[:hours_per_round] * 3600
|
||||
# Select words
|
||||
words = Enum.take_random(words(), num_words)
|
||||
# Try to start the round - if it's already running then that's OK
|
||||
case Wordbot.Db.start_round(channel, words, duration) do
|
||||
:ok -> Logger.debug("Started new wordbot round for #{channel}")
|
||||
{:error, :game_running} -> Logger.debug("Wordbot game already running for #{channel}")
|
||||
end
|
||||
|
||||
# Try to start a watcher if there isn't one running
|
||||
if !has_watcher?(channel),
|
||||
do: start_watcher(irc, channel)
|
||||
end
|
||||
|
||||
defp start_watcher(irc, channel) do
|
||||
# Start a watcher for the given channel
|
||||
Logger.debug("Starting wordbot game watcher for #{channel}")
|
||||
# Assert that there isn't a running watcher for the current channel
|
||||
false = has_watcher?(channel)
|
||||
task = Task.Supervisor.async_nolink(
|
||||
Wordbot.Watchers,
|
||||
fn -> watch_game(irc, channel) end,
|
||||
[shutdown: :brutal_kill]
|
||||
)
|
||||
add_watcher(channel, task)
|
||||
end
|
||||
|
||||
defp watch_game(irc, channel) do
|
||||
# Poll every second to check if a game is finished
|
||||
if Wordbot.Db.game_active?(channel) do
|
||||
Process.sleep(1000)
|
||||
watch_game(irc, channel)
|
||||
else
|
||||
finish_round(irc, channel)
|
||||
end
|
||||
end
|
||||
|
||||
def finish_round(irc, channel) do
|
||||
Logger.debug("Finishing wordbot round for #{channel}")
|
||||
|
||||
# Announce scores
|
||||
Irc.send_to(irc, channel, "Game over. Here were the scores:")
|
||||
scores = Wordbot.Db.scores(channel)
|
||||
|> Enum.sort_by(&(&1.score))
|
||||
|> Enum.reverse()
|
||||
|
||||
# Ranking is a little weird because we want to rank people so that having
|
||||
# the same score will give the same ranking, e.g.
|
||||
# 1. user1. 4
|
||||
# 2. user2. 3
|
||||
# 2. user3. 3
|
||||
# 3. user4. 1
|
||||
rankings = scores
|
||||
|> Enum.map(&(&1.score))
|
||||
|> Enum.sort()
|
||||
|> Enum.uniq()
|
||||
|> Enum.reverse()
|
||||
|> Enum.with_index()
|
||||
|> Map.new()
|
||||
|> IO.inspect()
|
||||
|
||||
Enum.each(scores, &Irc.send_to(irc, channel, "#{rankings[&1.score] + 1}. #{Util.denotify_nick(&1.user)}. #{&1.score}"))
|
||||
|
||||
# Stop the watcher, start new round
|
||||
delete_watcher(channel)
|
||||
start_round(irc, channel)
|
||||
end
|
||||
|
||||
## Plugin callbacks
|
||||
|
||||
@impl true
|
||||
def on_init(cfg) do
|
||||
Wordbot.Db.ensure_db()
|
||||
words = File.read!(cfg[:wordbot_source])
|
||||
|> String.split("\n")
|
||||
watchers = %{}
|
||||
{words, watchers}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(cfg) do
|
||||
children = [
|
||||
{Task.Supervisor, name: Omnibot.Contrib.Wordbot.Watchers, strategy: :one_for_one},
|
||||
Wordbot.Db.child_spec(cfg[:wordbot_db]),
|
||||
{Wordbot.Bot, cfg: cfg, name: Omnibot.Contrib.Wordbot.Bot},
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_all)
|
||||
def on_channel_msg(irc, channel, nick, msg) do
|
||||
words = Regex.split(@split_pattern, msg) |> MapSet.new()
|
||||
game_words = Wordbot.Db.unmatched_words(channel) |> MapSet.new()
|
||||
MapSet.intersection(words, game_words)
|
||||
|> Enum.each(fn word ->
|
||||
Wordbot.Db.add_score(channel, nick, word, msg)
|
||||
Irc.send_to(irc, channel, "#{nick}: Congrats! '#{word}' is good for 1 point.")
|
||||
end)
|
||||
end
|
||||
|
||||
def on_msg(irc, msg), do: Wordbot.Bot.on_msg(irc, msg)
|
||||
|
||||
def on_channel_msg(irc, channel, nick, msg), do: Wordbot.Bot.on_channel_msg(irc, channel, nick, msg)
|
||||
@impl true
|
||||
def on_join(irc, channel, who) do
|
||||
# Attempt to start a new round
|
||||
if State.cfg().nick == who do
|
||||
start_round(irc, channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user