Achievements: new dumb feature, add irc achievements

stats on users are tracked, and achievements are defined by writing
sql queries against those stats. silly fun
This commit is contained in:
Brian S. Stephan 2011-10-21 17:01:49 -05:00
parent 8c1ffc54ba
commit ef66c855f3
1 changed files with 479 additions and 0 deletions

479
modules/Achievements.py Normal file
View File

@ -0,0 +1,479 @@
"""
Achievements - gamifying IRC
Copyright (C) 2011 Brian S. Stephan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import re
import sqlite3
import thread
import time
from Module import Module
from extlib import irclib
__author__ = "Brian S. Stephan"
__copyright__ = "Copyright 2011, Brian S. Stephan"
__credits__ = ["Brian S. Stephan", "#lh"]
__license__ = "GPL"
__version__ = "0.1"
__maintainer__ = "Brian S. Stephan"
__email__ = "bss@incorporeal.org"
__status__ = "Development"
class Achievements(Module):
"""Give out achievements for doing stuff on IRC, because why not."""
class AchievementsSettings():
"""Track system settings."""
pass
def __init__(self, irc, config, server):
"""Set up trigger regexes."""
# TODO
joinpattern = '^!achievements\s+join$'
leavepattern = '^!achievements\s+leave$'
infopattern = '^!achievements\s+info\s+(.*)$'
rankpattern = '^!achievements\s+rank\s+(.*)$'
self.joinre = re.compile(joinpattern)
self.leavere = re.compile(leavepattern)
self.infore = re.compile(infopattern)
self.rankre = re.compile(rankpattern)
Module.__init__(self, irc, config, server)
self.connection = None
self.next_achievements_scan = 0
thread.start_new_thread(self.thread_do, ())
def db_init(self):
"""Set up the database tables, if they don't exist."""
version = self.db_module_registered(self.__class__.__name__)
if (version == None):
# have to create the database tables
db = self.get_db()
try:
version = 1
db.execute('''
CREATE TABLE achievements_player (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nick TEXT NOT NULL UNIQUE,
userhost TEXT NOT NULL DEFAULT '',
is_playing INTEGER NOT NULL DEFAULT 0,
last_seen_time TEXT DEFAULT CURRENT_TIMESTAMP
)''')
db.execute('''
CREATE TABLE achievements_event (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_id INTEGER NOT NULL,
event TEXT NOT NULL,
target TEXT,
msg_len INTEGER,
event_time TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(player_id) REFERENCES achievements_player(id)
)''')
db.execute('''
CREATE TABLE achievements_achievement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
desc TEXT NOT NULL,
query TEXT NOT NULL
)''')
db.execute('''
CREATE TABLE achievements_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_id INTEGER NOT NULL,
achievement_id INTEGER NOT NULL,
event_time TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(player_id) REFERENCES achievements_player(id),
FOREIGN KEY(achievement_id) REFERENCES achievements_achievement(id)
)''')
db.commit()
db.close()
self.db_register_module_version(self.__class__.__name__, version)
except sqlite3.Error as e:
db.rollback()
db.close()
print("sqlite error: " + str(e))
raise
if (version < 2):
db = self.get_db()
try:
version = 2
db.execute('''
CREATE TABLE achievements_config (
channel TEXT NOT NULL
)''')
db.commit()
db.close()
self.db_register_module_version(self.__class__.__name__, version)
except sqlite3.Error as e:
db.rollback()
db.close()
print("sqlite error: " + str(e))
raise
if (version < 3):
db = self.get_db()
try:
version = 3
db.execute('''
CREATE TABLE achievements_filter (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filter TEXT NOT NULL
)''')
db.execute('''
CREATE TABLE achievements_filter_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filter_id INTEGER NOT NULL,
event_id INTEGER NOT NULL,
FOREIGN KEY(filter_id) REFERENCES achievements_filter(id),
FOREIGN KEY(event_id) REFERENCES achievements_event(id)
)''')
db.commit()
db.close()
self.db_register_module_version(self.__class__.__name__, version)
except sqlite3.Error as e:
db.rollback()
db.close()
print("sqlite error: " + str(e))
raise
def register_handlers(self):
"""Handle all sorts of things to track."""
self.server.add_global_handler('pubmsg', self.track_irc_event)
self.server.add_global_handler('pubnotice', self.track_irc_event)
self.server.add_global_handler('privmsg', self.track_irc_event)
self.server.add_global_handler('privnotice', self.track_irc_event)
self.server.add_global_handler('join', self.track_irc_event)
self.server.add_global_handler('kick', self.track_irc_event)
self.server.add_global_handler('mode', self.track_irc_event)
self.server.add_global_handler('part', self.track_irc_event)
self.server.add_global_handler('quit', self.track_irc_event)
self.server.add_global_handler('invite', self.track_irc_event)
self.server.add_global_handler('action', self.track_irc_event)
self.server.add_global_handler('topic', self.track_irc_event)
self.server.add_global_handler('pubmsg', self.on_pub_or_privmsg)
self.server.add_global_handler('privmsg', self.on_pub_or_privmsg)
def unregister_handlers(self):
self.server.remove_global_handler('pubmsg', self.track_irc_event)
self.server.remove_global_handler('pubnotice', self.track_irc_event)
self.server.remove_global_handler('privmsg', self.track_irc_event)
self.server.remove_global_handler('privnotice', self.track_irc_event)
self.server.remove_global_handler('join', self.track_irc_event)
self.server.remove_global_handler('kick', self.track_irc_event)
self.server.remove_global_handler('mode', self.track_irc_event)
self.server.remove_global_handler('part', self.track_irc_event)
self.server.remove_global_handler('quit', self.track_irc_event)
self.server.remove_global_handler('invite', self.track_irc_event)
self.server.remove_global_handler('action', self.track_irc_event)
self.server.remove_global_handler('topic', self.track_irc_event)
self.server.remove_global_handler('pubmsg', self.on_pub_or_privmsg)
self.server.remove_global_handler('privmsg', self.on_pub_or_privmsg)
def track_irc_event(self, connection, event):
"""Put events in the database."""
self.connection = connection
if event.source():
if event.source().find('!') >= 0:
nick = irclib.nm_to_n(event.source())
userhost = irclib.nm_to_uh(event.source())
print('good: ' + nick + ' ' + userhost + ' ' + event.eventtype() + ' ' + str(event.target()))
if event.arguments():
msg = event.arguments()[0]
msg_len = len(event.arguments()[0])
else:
msg = ''
msg_len = 0
player_id = self._get_or_add_player(nick, userhost)
self._add_event(player_id, event.eventtype(), event.target(), msg, msg_len)
else:
print('bad: ' + event.source() + ' ' + event.eventtype())
else:
print('really bad: ' + event.eventtype())
def do(self, connection, event, nick, userhost, what, admin_unlocked):
"""Do stuff when commanded."""
if self.joinre.search(what):
return self.reply(connection, event, self._join_system(nick))
elif self.leavere.search(what):
return self.reply(connection, event, self._leave_system(nick))
elif self.infore.search(what):
match = self.infore.search(what)
achievement = match.group(1)
desc = self._get_achievement_info(achievement)
if desc:
return self.reply(connection, event, achievement + ': ' + desc)
elif self.rankre.search(what):
match = self.rankre.search(what)
player = match.group(1)
achievements = self._get_player_achievements(player)
if len(achievements):
return self.reply(connection, event, player + ' has obtained ' + ', '.join(achievements))
def thread_do(self):
"""Do the scan for achievements and other miscellaneous tasks."""
while not self.is_shutdown:
self._do_achievement_scan()
time.sleep(1)
def _get_or_add_player(self, nick, userhost):
"""Add a player to the database, or update the existing one, and return the id."""
try:
db = self.get_db()
cur = db.cursor()
statement = '''
INSERT OR IGNORE INTO achievements_player (nick) VALUES (?)'''
cur.execute(statement, (nick,))
statement = '''
UPDATE achievements_player SET userhost = ?, last_seen_time = CURRENT_TIMESTAMP
WHERE nick = ?'''
cur.execute(statement, (userhost, nick))
db.commit()
statement = '''SELECT id FROM achievements_player WHERE nick = ?'''
cur = db.execute(statement, (nick,))
result = cur.fetchone()
db.close()
return result['id']
except sqlite3.Error as e:
db.close()
print('sqlite error: ' + str(e))
raise
def _add_event(self, player_id, event, target, msg, msg_len):
"""Add an event to the log."""
try:
db = self.get_db()
cur = db.cursor()
statement = '''
INSERT INTO achievements_event (
player_id, event, target, msg_len
) VALUES (?, ?, ?, ?)
'''
cur.execute(statement, (player_id, event, target, msg_len))
db.commit()
event_id = cur.lastrowid
# now see if the event matched any filters
query = '''
SELECT id FROM achievements_filter WHERE ? REGEXP filter
'''
cursor = db.execute(query, (msg.decode('utf-8', 'replace'),))
results = cursor.fetchall()
for result in results:
cur = db.cursor()
statement = '''
INSERT INTO achievements_filter_log (filter_id, event_id)
VALUES (?, ?)
'''
cur.execute(statement, (result['id'], event_id))
db.commit()
db.close()
return event_id
except sqlite3.Error as e:
db.close()
print('sqlite error: ' + str(e))
raise
def _get_achievements_settings(self):
"""Get the report settings."""
try:
# get the settings
db = self.get_db()
query = 'SELECT channel FROM achievements_config'
cursor = db.execute(query)
result = cursor.fetchone()
db.close()
if result:
settings = self.AchievementsSettings()
settings.channel = result['channel']
return settings
except sqlite3.Error as e:
db.close()
print('sqlite error: ' + str(e))
raise
def _join_system(self, nick):
"""Add the appropriate nick to the game."""
try:
db = self.get_db()
cur = db.cursor()
statement = 'UPDATE achievements_player SET is_playing = 1 WHERE nick = ?'
cur.execute(statement, (nick,))
db.commit()
db.close()
return nick + ' joined.'
except sqlite3.Error as e:
db.close()
print('sqlite error: ' + str(e))
raise
def _leave_system(self, nick):
"""Remove the appropriate nick from the game."""
try:
db = self.get_db()
cur = db.cursor()
statement = 'UPDATE achievements_player SET is_playing = 0 WHERE nick = ?'
cur.execute(statement, (nick,))
db.commit()
db.close()
return nick + ' left.'
except sqlite3.Error as e:
db.close()
print('sqlite error: ' + str(e))
raise
def _add_player_to_achievement_log(self, player_id, achievement_id):
"""Log the success of a player."""
try:
db = self.get_db()
cur = db.cursor()
statement = 'INSERT INTO achievements_log (player_id, achievement_id) VALUES (?, ?)'
cur.execute(statement, (player_id, achievement_id))
db.commit()
db.close()
return
except sqlite3.Error as e:
db.close()
print('sqlite error: ' + str(e))
raise
def _get_achievement_info(self, achievement):
"""Return the description of a given achievement."""
try:
db = self.get_db()
query = 'SELECT desc FROM achievements_achievement WHERE name = ?'
cursor = db.execute(query, (achievement,))
result = cursor.fetchone()
db.close()
if result:
return result['desc']
except sqlite3.Error as e:
db.close()
print('sqlite error: ' + str(e))
raise
def _get_player_achievements(self, nick):
"""Return the achievements the nick has."""
achievements = []
try:
db = self.get_db()
query = '''
SELECT a.name FROM achievements_achievement a
INNER JOIN achievements_log l ON l.achievement_id = a.id
INNER JOIN achievements_player p ON p.id = l.player_id
WHERE p.nick = ?
'''
cursor = db.execute(query, (nick,))
results = cursor.fetchall()
db.close()
for result in results:
achievements.append(result['name'])
return achievements
except sqlite3.Error as e:
db.close()
print('sqlite error: ' + str(e))
raise
def _do_achievement_scan(self):
"""Run the queries in the database, seeing if anyone new has an achievement."""
# don't do anything the first time
if self.next_achievements_scan == 0:
self.next_achievements_scan = time.time() + 300
if self.next_achievements_scan < time.time():
self.next_achievements_scan = time.time() + 300
if self.connection is None:
return
settings = self._get_achievements_settings()
channel = settings.channel
achievers = self._query_for_new_achievers()
for achiever in achievers:
self.sendmsg(self.connection, channel, achiever[0] + ' achieved ' + achiever[1] + '!')
def _query_for_new_achievers(self):
"""Get new achievement earners for each achievement."""
achievers = []
try:
db = self.get_db()
query = 'SELECT id, name, query FROM achievements_achievement'
cursor = db.execute(query)
achievements = cursor.fetchall()
for achievement in achievements:
print('checking achievement:[' + achievement['name'] + ']')
query = '''
SELECT p.id, p.nick FROM achievements_player p WHERE
p.nick IN (
'''
query = query + achievement['query']
query = query + '''
) AND p.is_playing = 1
AND p.id NOT IN (
SELECT player_id FROM achievements_log l
INNER JOIN achievements_achievement a
ON a.id = l.achievement_id
WHERE a.name = ?
)
'''
cursor = db.execute(query, (achievement['name'],))
ach_achievers = cursor.fetchall()
for ach_achiever in ach_achievers:
print('name:[' + ach_achiever['nick'] + '] achievement:[' + achievement['name'] + ']')
self._add_player_to_achievement_log(ach_achiever['id'], achievement['id'])
achievers.append((ach_achiever['nick'], achievement['name']))
db.close()
return achievers
except sqlite3.Error as e:
db.close()
print('sqlite error: ' + str(e))
raise
# vi:tabstop=4:expandtab:autoindent