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>
184 lines
5.0 KiB
Elixir
184 lines
5.0 KiB
Elixir
defmodule Omnibot.Contrib.Wordbot do
|
|
use Omnibot.Plugin.Supervisor
|
|
alias Omnibot.{Contrib.Wordbot, State, Util}
|
|
require Logger
|
|
|
|
@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)
|
|
#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 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
|