WIP: Wordbot

Wordbot is a little more complex of a bot module and I've been working
on it here.

Other than wordbot module, a few minor tweaks have been added all around
that don't really affect anything.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2020-07-01 12:04:51 -07:00
parent 871b7771fe
commit 67ba7a5847
13 changed files with 332 additions and 5 deletions

View File

@@ -0,0 +1,35 @@
defmodule Omnibot.Contrib.Wordbot.Bot do
use Omnibot.Module
alias Omnibot.Contrib.Wordbot
@split_pattern ~r/[\s\b]+/
command "!wordbot", ["leaderboard"] do
Irc.send_to(irc, channel, "leaderboard logic here")
end
@impl true
def on_init(cfg) do
Wordbot.Db.ensure_db()
File.read!(cfg[:wordbot_source])
|> String.split("\n")
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
# TODO start games
# * Tasks for watching games(?)
end
end

189
lib/contrib/wordbot/db.ex Normal file
View File

@@ -0,0 +1,189 @@
defmodule Omnibot.Contrib.Wordbot.Db do
alias Omnibot.Util
# SQL for creating the new database
@database_sql ~S"""
CREATE TABLE IF NOT EXISTS game (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
start INTEGER NOT NULL,
end INTEGER NOT NULL,
channel VARCHAR(40) NOT NULL
);
CREATE TABLE IF NOT EXISTS word (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
game INTEGER NOT NULL,
word VARCHAR(40) NOT NULL,
FOREIGN KEY (game) REFERENCES game(id),
UNIQUE(game, word)
);
CREATE TABLE IF NOT EXISTS score (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
game INTEGER NOT NULL,
word INTEGER NOT NULL,
user VARCHAR(40) NOT NULL,
line VARCHAR(1024) NOT NULL,
FOREIGN KEY (game) REFERENCES game(id),
FOREIGN KEY (word) REFERENCES word(id),
UNIQUE(game, word)
);
"""
def ensure_db() do
Sqlitex.Server.exec(__MODULE__, @database_sql)
end
@doc "Gets the words for the current round in the given channel."
def words(channel) do
id = game_id!(channel)
{:ok, rows} =
Sqlitex.Server.query(__MODULE__, "SELECT word FROM word WHERE game = ?1", bind: [id])
for row <- rows, do: row[:word]
end
@doc """
Starts a new round for the given channel in the database.
## Options
* `:end_early` - if `true`, this will create a new game even though there is
a game already running. Default is false.
## Returns
* `:ok` if a new round was started.
* `{:error, :game_running}` if a game couldn't be started because one was already running.
"""
def start_round(channel, words, duration, opts \\ []) do
end_early = Access.get(opts, :end_early, false)
if !game_active?(channel) or end_early do
start = Util.now_unix()
end_ = start + duration
with {:ok, _} <-
Sqlitex.Server.query(
__MODULE__,
"INSERT INTO game (start, end, channel) VALUES (?1, ?2, ?3)",
bind: [start, end_, channel]
),
{:ok, game_id} = game_id(channel) do
Enum.each(
words,
fn word ->
{:ok, _} =
Sqlitex.Server.query(__MODULE__, "INSERT INTO word (game, word) VALUES(?1, ?2)",
bind: [game_id, word]
)
end
)
end
:ok
else
{:error, :game_running}
end
end
def game_active?(channel) do
case game_id(channel) do
{:ok, _id} -> true
{:error, :no_game} -> false
end
end
def child_spec(wordbot_db) do
%{
id: Sqlitex.Server,
start: {Sqlitex.Server, :start_link, [wordbot_db, [name: __MODULE__]]}
}
end
@doc """
Gets the ID of the currently running game.
## Returns
* {:ok, id} on success
* {:error, :no_game} when no game is running for this channel
"""
def game_id(channel) do
now = Util.now_unix()
id =
with {:ok, rows} <-
Sqlitex.Server.query(
__MODULE__,
"SELECT id FROM game WHERE channel = ?1 AND end > ?2 ORDER BY id DESC",
bind: [channel, now]
),
[id | _] <- Enum.map(rows, & &1[:id]),
do: id
case id do
[] -> {:error, :no_game}
id -> {:ok, id}
end
end
def game_id!(channel) do
{:ok, id} = game_id(channel)
id
end
def last_game_id(channel) do
id =
with {:ok, rows} <-
Sqlitex.Server.query(
__MODULE__,
"SELECT id FROM game WHERE channel = ?1 ORDER BY id DESC",
bind: [channel]
),
[id | _] <- for(row <- rows, do: row[:id]),
do: id
case id do
[] -> {:error, :no_game}
id -> {:ok, id}
end
end
def add_score(channel, user, word, line) do
id = game_id!(channel)
{:ok, _} = Sqlitex.Server.query(
__MODULE__,
"""
INSERT INTO score (game, word, user, line)
VALUES (?1, (SELECT word.id FROM word WHERE game = ?1 AND word = ?2), ?3, ?4)
""", bind: [id, word, user, line])
end
def scores(channel) do
id = game_id!(channel)
{:ok, rows} = Sqlitex.Server.query(
__MODULE__,
"""
SELECT user, COUNT(score.id) AS score FROM score
JOIN game ON score.game = game.id
WHERE game.id = ?1
GROUP BY user
""",
bind: [id]
)
Enum.map(rows, &Map.new/1)
end
@doc "Gets all words that have not been scored on from the given channel."
def unmatched_words(channel) do
id = game_id!(channel)
{:ok, rows} = Sqlitex.Server.query(
__MODULE__,
"""
SELECT word FROM word
WHERE word.game = ?1
AND id NOT IN (SELECT score.word FROM score WHERE game = ?1)
""",
bind: [id]
)
Enum.map(rows, &(&1[:word]))
end
end