dr.botzo/modules/Storycraft.py

1188 lines
51 KiB
Python

"""
Storycraft - collaborative nonsense story writing
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 random
import re
import time
from dateutil.tz import *
import MySQLdb as mdb
from Module import Module
__author__ = "Brian S. Stephan"
__copyright__ = "Copyright 2011, Brian S. Stephan"
__credits__ = ["Brian S. Stephan", "Mike Bloy", "#lh"]
__license__ = "GPL"
__version__ = "0.1"
__maintainer__ = "Brian S. Stephan"
__email__ = "bss@incorporeal.org"
__status__ = "Development"
class Storycraft(Module):
"""Play Storycraft, the game of varying authors adding to a story mostly devoid of context.
This is a game for people who like making up facts, insulting each other, telling
insane stories, making in-jokes, and generally having a good time writing what can
fairly only be described as pure nonsense. It does not assume that all players are
available at any given time, so games may take a while, but the module allows for
concurrency in the number of games running.
"""
class StorycraftGame():
"""Track a storycraft game details."""
pass
class StorycraftPlayer():
"""Track a storycraft player details."""
pass
class StorycraftLine():
"""Track a storycraft line details."""
pass
class StorycraftSettings():
"""Track the settings for the server."""
pass
def __init__(self, irc, config):
"""Set up trigger regexes."""
Module.__init__(self, irc, config)
rootcommand = '^!storycraft'
statuspattern = rootcommand + '\s+status$'
newgamepattern = rootcommand + '\s+new\s+game(\s+with\s+config\s+(.*)$|$)'
joingamepattern = rootcommand + '\s+game\s+(\d+)\s+join$'
listgamespattern = rootcommand + '\s+list\s+games\s+(open|in progress|completed|my games|waiting for me)$'
startgamepattern = rootcommand + '\s+game\s+(\d+)\s+start$'
showlinepattern = rootcommand + '\s+game\s+(\d+)\s+show\s+line$'
addlinepattern = rootcommand + '\s+game\s+(\d+)\s+add\s+line\s+(.*)$'
gamestatuspattern = rootcommand + '\s+game\s+(\d+)\s+status$'
exportpattern = rootcommand + '\s+game\s+(\d+)\s+export$'
self.statusre = re.compile(statuspattern)
self.newgamere = re.compile(newgamepattern)
self.joingamere = re.compile(joingamepattern)
self.listgamesre = re.compile(listgamespattern)
self.startgamere = re.compile(startgamepattern)
self.showlinere = re.compile(showlinepattern)
self.addlinere = re.compile(addlinepattern)
self.gamestatusre = re.compile(gamestatuspattern)
self.exportre = re.compile(exportpattern)
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
cur = db.cursor(mdb.cursors.DictCursor)
cur.execute('''
CREATE TABLE storycraft_config (
master_channel VARCHAR(64) NOT NULL,
concurrent_games INTEGER NOT NULL,
default_round_mode INTEGER NOT NULL,
default_game_length INTEGER NOT NULL,
default_line_length INTEGER NOT NULL,
default_random_method INTEGER NOT NULL,
default_lines_per_turn INTEGER NOT NULL
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
''')
cur.execute('''
INSERT INTO storycraft_config
(master_channel, concurrent_games, default_round_mode,
default_game_length, default_line_length,
default_random_method, default_lines_per_turn)
VALUES ('#dr.botzo', 10, 1, 20, 140, 1, 2)
''')
cur.execute('''
CREATE TABLE storycraft_game (
id SERIAL,
round_mode INTEGER NOT NULL,
game_length INTEGER NOT NULL,
line_length INTEGER NOT NULL,
random_method INTEGER NOT NULL,
lines_per_turn INTEGER NOT NULL,
status VARCHAR(16) NOT NULL,
owner_nick VARCHAR(64) NOT NULL,
owner_userhost VARCHAR(256) NOT NULL,
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP NULL DEFAULT NULL
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
''')
cur.execute('''
CREATE TABLE storycraft_player (
id SERIAL,
game_id BIGINT(20) UNSIGNED NOT NULL,
nick VARCHAR(64) NOT NULL,
userhost VARCHAR(256) NOT NULL,
FOREIGN KEY(game_id) REFERENCES storycraft_game(id)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
''')
cur.execute('''
CREATE TABLE storycraft_line (
id SERIAL,
game_id BIGINT(20) UNSIGNED NOT NULL,
player_id BIGINT(20) UNSIGNED NOT NULL,
line LONGTEXT NOT NULL,
time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(game_id) REFERENCES storycraft_game(id),
FOREIGN KEY(player_id) REFERENCES storycraft_player(id)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
''')
db.commit()
self.db_register_module_version(self.__class__.__name__, version)
except mdb.Error as e:
db.rollback()
self.log.error("database error trying to create tables")
self.log.exception(e)
raise
finally: cur.close()
def do(self, connection, event, nick, userhost, what, admin_unlocked):
"""Pass storycraft control commands to the appropriate method based on input."""
if self.statusre.search(what):
return self.irc.reply(event, self.storycraft_status(connection, event, nick, userhost, what, admin_unlocked))
elif self.newgamere.search(what):
return self.irc.reply(event, self.storycraft_newgame(connection, event, nick, userhost, what, admin_unlocked))
elif self.joingamere.search(what):
return self.irc.reply(event, self.storycraft_joingame(connection, event, nick, userhost, what, admin_unlocked))
elif self.listgamesre.search(what):
return self.irc.reply(event, self.storycraft_listgames(connection, event, nick, userhost, what, admin_unlocked))
elif self.startgamere.search(what):
return self.irc.reply(event, self.storycraft_startgame(connection, event, nick, userhost, what, admin_unlocked))
elif self.showlinere.search(what):
return self.irc.reply(event, self.storycraft_showline(connection, event, nick, userhost, what, admin_unlocked))
elif self.addlinere.search(what):
return self.irc.reply(event, self.storycraft_addline(connection, event, nick, userhost, what, admin_unlocked))
elif self.gamestatusre.search(what):
return self.irc.reply(event, self.storycraft_gamestatus(connection, event, nick, userhost, what, admin_unlocked))
elif self.exportre.search(what):
return self.irc.reply(event, self.storycraft_export(connection, event, nick, userhost, what, admin_unlocked))
def storycraft_status(self, connection, event, nick, userhost, what, admin_unlocked):
"""Print information about the storycraft games, or one specific game."""
match = self.statusre.search(what)
if match:
# do the general status of all games
count_completed = self._get_completed_game_count()
count_in_progress = self._get_in_progress_game_count()
count_open = self._get_open_game_count()
count_free = self._get_free_game_count()
return 'Storycraft {0:s} - {1:d} games completed, {2:d} in progress, {3:d} open. {4:d} slots free.'.format(__version__, count_completed, count_in_progress, count_open, count_free)
def storycraft_gamestatus(self, connection, event, nick, userhost, what, admin_unlocked):
"""Print information about one specific game."""
match = self.gamestatusre.search(what)
if match:
if match.group(1):
game_id = int(match.group(1))
# do the status of one game
game = self._get_game_details(game_id)
if game:
# get game string
status_str = self._get_game_summary(game)
return status_str
else:
return 'Game #{0:d} does not exist.'.format(game_id)
def storycraft_newgame(self, connection, event, nick, userhost, what, admin_unlocked):
"""Add a game to the system, which users can join."""
match = self.newgamere.search(what)
if match:
# ensure the system isn't at capacity
count_free = self._get_free_game_count()
if count_free > 0:
# get the default settings
settings = self._get_storycraft_settings()
master_channel = settings.master_channel
round_mode = settings.default_round_mode
game_length = settings.default_game_length
line_length = settings.default_line_length
random_method = settings.default_random_method
lines_per_turn = settings.default_lines_per_turn
# check for custom options
if match.group(2):
options = match.group(2)
# override settings with provided options
tuples = options.split(';')
for option in tuples:
optpair = option.split(',')
if len(optpair) == 2:
if optpair[0] == 'round_mode':
round_mode = int(optpair[1])
elif optpair[0] == 'game_length':
game_length = int(optpair[1])
elif optpair[0] == 'line_length':
line_length = int(optpair[1])
elif optpair[0] == 'random_method':
random_method = int(optpair[1])
elif optpair[0] == 'lines_per_turn':
lines_per_turn = int(optpair[1])
# add a new game
game_id = self._add_new_game(round_mode, game_length, line_length, random_method, lines_per_turn, nick, userhost)
if game_id:
# add the player to the game, too
self._add_player_to_game(game_id, nick, userhost)
# tell the control channel
self.sendmsg(connection, master_channel, '{0:s} created a game of storycraft - do \'!storycraft game {1:d} join\' to take part!'.format(nick, game_id))
self.log.debug("{0:s} added a new game".format(nick))
return 'Game #{0:d} has been created. When everyone has joined, do \'!storycraft game {0:d} start\''.format(game_id)
else:
return 'Error creating game.'
else:
return 'All slots are full.'
def storycraft_joingame(self, connection, event, nick, userhost, what, admin_unlocked):
"""Add a player to an open game."""
match = self.joingamere.search(what)
if match:
game_id = int(match.group(1))
# ensure the game exists
game = self._get_game_details(game_id)
if game:
# check that the game hasn't started yet
if game.status == 'OPEN':
# see if userhost is already in the game
if not self._get_player_exists_in_game(game_id, userhost):
# add the player
if self._add_player_to_game(game_id, nick, userhost):
# output results
# get the default settings
settings = self._get_storycraft_settings()
master_channel = settings.master_channel
self.sendmsg(connection, master_channel, '{0:s} joined storycraft #{1:d}!'.format(nick, game_id))
self.log.debug("{0:s} joined game #{1:d}".format(nick, game_id))
return '{0:s}, welcome to the game.'.format(nick)
else:
return 'Error joining game.'
else:
return 'You are already in game #{0:d}.'.format(game_id)
else:
return 'Game #{0:d} is not open for new players.'.format(game_id)
else:
return 'Game #{0:d} does not exist.'.format(game_id)
def storycraft_listgames(self, connection, event, nick, userhost, what, admin_unlocked):
"""Get the listing of either open or in progress games."""
match = self.listgamesre.search(what)
if match:
category = match.group(1)
if category == 'open':
games = self._get_open_games()
elif category == 'in progress':
games = self._get_in_progress_games()
elif category == 'completed':
games = self._get_completed_games()
elif category == 'my games':
games = self._get_active_games_with_player(nick)
elif category == 'waiting for me':
games = self._get_games_waiting_on_player(nick)
if len(games) > 5:
# just list the game ids
gameids = []
for game in games:
gameids.append(str(game.id))
return 'Too many to list! ids: ' + ','.join(gameids)
else:
# show game details, since there's not many
gamestrs = []
for game in games:
gamestrs.append(self._get_game_summary(game))
return '\n'.join(gamestrs)
def storycraft_startgame(self, connection, event, nick, userhost, what, admin_unlocked):
"""Start a game, closing the period to join and starting line trading."""
match = self.startgamere.search(what)
if match:
game_id = int(match.group(1))
# retrieve the game
game = self._get_game_details(game_id)
if game:
# check that the user is the owner of the game
if userhost == game.owner_userhost:
# check that the game is open
if 'OPEN' == game.status:
# start the game
if self._start_game(game_id):
# determine the first person to send a line
if self._pick_next_player(game_id):
# indicate the status
game = self._get_game_details(game_id)
line = self._get_lines_for_game(game_id)[0]
player = self._get_player_by_id(line.player_id)
# get the default settings
settings = self._get_storycraft_settings()
master_channel = settings.master_channel
# tell the control channel
self.sendmsg(connection, master_channel, '{0:s} started storycraft #{1:d}! - first player is {2:s}, do \'!storycraft game {1:d} show line\' when the game is assigned to you.'.format(nick, game_id, player.nick))
self.log.debug("{0:s} started game #{1:d}".format(nick, game_id))
return 'Game #{0:d} started.'.format(game_id)
else:
return 'Failed to assign game to first player.'
else:
return 'Could not start game.'
else:
return 'Game #{0:d} is not open.'.format(game_id)
else:
return 'You are not the owner of #{0:d}.'.format(game_id)
else:
return 'Game #{0:d} does not exist.'.format(game_id)
def storycraft_showline(self, connection, event, nick, userhost, what, admin_unlocked):
"""List the line to continue with, if queryer is the assignee."""
match = self.showlinere.search(what)
if match:
game_id = int(match.group(1))
# retrieve the game
game = self._get_game_details(game_id)
if game:
# check that the game is in progress
if 'IN PROGRESS' == game.status:
# get the most recent line
lines = self._get_lines_for_game(game_id)
if lines:
line = lines[0]
# check that the line is a prompt for input
if line.line == '':
player = self._get_player_by_id(line.player_id)
# check that the caller is the person the line is assigned to
if player.userhost == userhost:
# check previous line
if len(lines) > 1:
# get previous line and display it
repline = lines[1]
return 'The current line is \'{0:s}\'. Continue the story with \'!storycraft game {1:d} add line YOUR TEXT\'.'.format(repline.line, game_id)
else:
# player is starting the story
return 'You are starting the story! Add the first line with \'!storycraft game {0:d} add line YOUR TEXT\'.'.format(game_id)
else:
return 'You are not the assignee of the current game.'
else:
return 'Game is in progress but the most recent line is not available - internal consistency error.'
else:
return 'Game #{0:d} has no lines - internal consistency error.'.format(game_id)
else:
return 'Game #{0:d} has not been started.'.format(game_id)
else:
return 'Game #{0:d} does not exist.'.format(game_id)
def storycraft_addline(self, connection, event, nick, userhost, what, admin_unlocked):
"""Add a line to an in progress game."""
match = self.addlinere.search(what)
if match:
game_id = int(match.group(1))
input_line = match.group(2)
# retrieve the game
game = self._get_game_details(game_id)
if game:
# check that the game is in progress
if 'IN PROGRESS' == game.status:
# get the most recent line
lines = self._get_lines_for_game(game_id)
if lines:
line = lines[0]
# check that the line is a prompt for input
if line.line == '':
player = self._get_player_by_id(line.player_id)
if player.userhost == userhost:
# check the input line length
if len(input_line) <= game.line_length:
# add line
if self._update_line(line.id, input_line):
# determine the first person to send a line
if self._pick_next_player(game_id):
# indicate the status
game = self._get_game_details(game_id)
line = self._get_lines_for_game(game_id)[0]
last_line = self._get_lines_for_game(game_id)[1]
player = self._get_player_by_id(line.player_id)
return_msg = 'Line logged.'
# get progress, so user knows how much time is left
progress_str = self._get_progress_string_for_game(game)
# message the new owner
if player.nick == nick:
# simpily notify them they're up again
return_msg = 'Line logged. Please add another. ' + progress_str
else:
# notify the new owner, too
self.sendmsg(connection, player.nick, 'You have a new line in storycraft #{0:d}: \'{1:s}\' {2:s}'.format(game_id, last_line.line, progress_str))
# get the default settings
settings = self._get_storycraft_settings()
master_channel = settings.master_channel
self.log.debug("{0:s} added a line to #{1:d}".format(nick, game_id))
# log output
if game.status == 'IN PROGRESS':
self.sendmsg(connection, master_channel, '{0:s} added a line to storycraft #{1:d}! - next player is {2:s}'.format(nick, game_id, player.nick))
return return_msg
else:
self.sendmsg(connection, master_channel, '{0:s} finished storycraft #{1:d}!'.format(nick, game_id))
return 'Line logged (and game completed).'
else:
return 'Failed to assign game to next player.'
else:
return 'Failed to save your line.'
else:
return 'The maximum line length for this game is {0:d} characters. (You were over by {1:d}.)'.format(game.line_length, len(input_line)-game.line_length)
else:
return 'You are not the assignee of the current game.'
else:
return 'Game is in progress but the most recent line is not available - internal consistency error.'
else:
return 'Game #{0:d} has no lines - internal consistency error.'.format(game_id)
else:
return 'Game #{0:d} has not been started.'.format(game_id)
else:
return 'Game #{0:d} does not exist.'.format(game_id)
def storycraft_export(self, connection, event, nick, userhost, what, admin_unlocked):
"""Provide the story for access outside of the bot."""
match = self.exportre.search(what)
if match:
game_id = int(match.group(1))
# retrieve the game
game = self._get_game_details(game_id)
if game:
# check that the game is completed
if 'COMPLETED' == game.status:
# get the most recent line
lines = self._get_lines_for_game(game_id)
if lines:
# write the story to disk
fn = self._export_game_to_disk(game, lines)
if fn:
return 'Story written as {0:s}.'.format(fn)
else:
return 'Error writing story to disk.'
else:
return 'Game #{0:d} has no lines - internal consistency error.'.format(game_id)
else:
return 'Game #{0:d} has not been completed.'.format(game_id)
else:
return 'Game #{0:d} does not exist.'.format(game_id)
def _get_game_details(self, game_id):
"""Get the details of one game."""
db = self.get_db()
try:
cur = db.cursor(mdb.cursors.DictCursor)
# get the specified game and populate a StorycraftGame
query = '''
SELECT id, round_mode, game_length, line_length, random_method,
lines_per_turn, status, owner_nick, owner_userhost, start_time,
end_time
FROM storycraft_game WHERE id = %s
'''
cur.execute(query, (game_id,))
result = cur.fetchone()
if result:
game = self.StorycraftGame()
game.id = int(result['id'])
game.round_mode = int(result['round_mode'])
game.game_length = int(result['game_length'])
game.line_length = int(result['line_length'])
game.random_method = int(result['random_method'])
game.lines_per_turn = int(result['lines_per_turn'])
game.status = result['status']
game.owner_nick = result['owner_nick']
game.owner_userhost = result['owner_userhost']
game.start_time = result['start_time'].replace(tzinfo=tzlocal())
game.end_time = result['end_time']
if game.end_time is not None:
game.end_time = game.end_time.replace(tzinfo=tzlocal())
return game
except mdb.Error as e:
self.log.error("database error during get game details")
self.log.exception(e)
raise
finally: cur.close()
def _get_game_summary(self, game):
"""Display game info for a general summary."""
status_str = '#{0:d} - created on {1:s} by {2:s}, {3:s}'.format(game.id,
game.start_time.strftime('%Y/%m/%d %H:%M:%S'),
game.owner_nick, game.status)
if game.status == 'COMPLETED' and game.end_time:
status_str = status_str + ' ({0:s})'.format(game.end_time.strftime('%Y/%m/%d %H:%M:%S'))
elif game.status == 'IN PROGRESS':
lines = self._get_lines_for_game(game.id)
player = self._get_player_by_id(lines[0].player_id)
status_str = status_str + ' ({0:d} lines, next is {1:s})'.format(len(lines)-1, player.nick)
# get players in game
player_names = []
players = self._get_player_list_for_game(game.id)
if players:
for player in players:
player_names.append(player.nick)
status_str = status_str + ', players: {0:s}'.format(', '.join(player_names))
status_str = status_str + '. (o:{0:d}[{1:d}],{2:d},{3:d},{4:d})'.format(game.round_mode, game.game_length, game.line_length, game.random_method, game.lines_per_turn)
return status_str
def _get_progress_string_for_game(self, game):
"""Get a terse summary of the game's progress."""
if game.round_mode == 1:
lines = self._get_lines_for_game(game.id)
num_lines = len(lines)-1
progress = '(lines: {0:d}/{1:d}'.format(num_lines, game.game_length)
if num_lines == game.game_length - 1:
progress = progress + ' LAST LINE'
progress = progress + ')'
return progress
else:
return ''
def _add_new_game(self, round_mode, game_length, line_length, random_method, lines_per_turn, nick, userhost):
"""Add a new game to the system."""
db = self.get_db()
try:
cur = db.cursor(mdb.cursors.DictCursor)
statement = '''
INSERT INTO storycraft_game (
round_mode, game_length, line_length, random_method,
lines_per_turn, status, owner_nick, owner_userhost
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
'''
cur.execute(statement, (round_mode, game_length, line_length,
random_method, lines_per_turn, 'OPEN', nick, userhost))
db.commit()
return cur.lastrowid
except mdb.Error as e:
db.rollback()
self.log.error("database error during add new game")
self.log.exception(e)
raise
finally: cur.close()
def _get_player_list_for_game(self, game_id):
"""Get the list of players in one game."""
players = []
db = self.get_db()
try:
# get the players for specified game and populate a list
cur = db.cursor(mdb.cursors.DictCursor)
query = 'SELECT id, game_id, nick, userhost FROM storycraft_player WHERE game_id = %s'
cur.execute(query, (game_id,))
results = cur.fetchall()
for result in results:
player = self.StorycraftPlayer()
player.id = result['id']
player.game_id = result['game_id']
player.nick = result['nick']
player.userhost = result['userhost']
players.append(player)
return players
except mdb.Error as e:
self.log.error("database error during getting player list for game")
self.log.exception(e)
raise
finally: cur.close()
def _get_game_exists(self, game_id):
"""Return the existence of a game."""
return self._get_game_details(game_id) is not None
def _start_game(self, game_id):
"""Start a game, if it's currently open."""
db = self.get_db()
try:
cur = db.cursor(mdb.cursors.DictCursor)
statement = '''
UPDATE storycraft_game SET status = 'IN PROGRESS' WHERE status = 'OPEN' AND id = %s
'''
cur.execute(statement, (game_id,))
db.commit()
return game_id
except mdb.Error as e:
db.rollback()
self.log.error("database error during start game")
self.log.exception(e)
raise
finally: cur.close()
def _pick_next_player(self, game_id):
"""Based on the game's settings and state, set who will be replying next."""
game = self._get_game_details(game_id)
if game:
# do round_mode stuff
if game.round_mode == 1:
count_by_latest_player = 0
latest_player_id = 0
# get the lines in this game, latest first
lines = self._get_lines_for_game(game_id)
for line in lines:
if latest_player_id == 0:
latest_player_id = line.player_id
if latest_player_id == line.player_id:
count_by_latest_player = count_by_latest_player + 1
else:
break
# now we know how many times the most recent person has responded,
# figure out what to do
if count_by_latest_player == 0:
# must be a new game, get a random player
players = self._get_player_list_for_game(game.id)
random_player = players[random.randint(1,len(players))-1]
return self._assign_game_to_player(game_id, random_player.id)
elif count_by_latest_player >= game.lines_per_turn and len(lines) < game.game_length:
# player has reached the max, get a random different player to assign
players = self._get_player_list_for_game(game.id)
random_player_id = latest_player_id
if len(players) > 1:
while random_player_id == latest_player_id:
random_player = players[random.randint(1,len(players))-1]
random_player_id = random_player.id
return self._assign_game_to_player(game_id, random_player_id)
elif count_by_latest_player >= game.lines_per_turn and len(lines) >= game.game_length:
# end of game
return self._end_game(game_id)
elif count_by_latest_player < game.lines_per_turn:
# player should continue getting lines
return self._assign_game_to_player(game_id, latest_player_id)
def _assign_game_to_player(self, game_id, player_id):
"""Assign the game to a player, prompting them for responses."""
db = self.get_db()
try:
cur = db.cursor(mdb.cursors.DictCursor)
statement = '''
INSERT INTO storycraft_line (game_id, player_id, line)
VALUES (%s, %s, %s)
'''
cur.execute(statement, (game_id, player_id, ''))
db.commit()
return cur.lastrowid
except mdb.Error as e:
db.rollback()
self.log.error("database error during assign game to player")
self.log.exception(e)
raise
finally: cur.close()
def _end_game(self, game_id):
"""End the given game, disallowing adding lines."""
db = self.get_db()
try:
cur = db.cursor(mdb.cursors.DictCursor)
statement = '''
UPDATE storycraft_game SET status = 'COMPLETED', end_time = CURRENT_TIMESTAMP
WHERE status = 'IN PROGRESS' AND id = %s
'''
cur.execute(statement, (game_id,))
db.commit()
return game_id
except mdb.Error as e:
db.rollback()
self.log.error("database error during end game")
self.log.exception(e)
raise
finally: cur.close()
def _get_player_by_id(self, player_id):
"""Get the player details based on id."""
db = self.get_db()
try:
# get the specified player and populate a StorycraftPlayer
cur = db.cursor(mdb.cursors.DictCursor)
query = 'SELECT id, game_id, nick, userhost FROM storycraft_player WHERE id = %s'
cur.execute(query, (player_id,))
result = cur.fetchone()
if result:
player = self.StorycraftPlayer()
player.id = result['id']
player.game_id = result['game_id']
player.nick = result['nick']
player.userhost = result['userhost']
return player
except mdb.Error as e:
self.log.error("database error during get player by id")
self.log.exception(e)
raise
finally: cur.close()
def _get_player_by_userhost_in_game(self, game_id, userhost):
"""Get the player details if they exist in the given game."""
db = self.get_db()
try:
# get the specified player if they exist in the game and populate a StorycraftPlayer
cur = db.cursor(mdb.cursors.DictCursor)
query = 'SELECT id, game_id, nick, userhost FROM storycraft_player WHERE game_id = %s AND userhost = %s'
cur.execute(query, (game_id,userhost))
result = cur.fetchone()
if result:
player = self.StorycraftPlayer()
player.id = result['id']
player.game_id = result['game_id']
player.nick = result['nick']
player.userhost = result['userhost']
return player
except mdb.Error as e:
self.log.error("database error during get player by userhost")
self.log.exception(e)
raise
finally: cur.close()
def _get_completed_games(self):
"""Get the games with COMPLETED status."""
return self._get_games_of_type('COMPLETED')
def _get_in_progress_games(self):
"""Get the games with IN PROGRESS status."""
return self._get_games_of_type('IN PROGRESS')
def _get_open_games(self):
"""Get the games with OPEN status."""
return self._get_games_of_type('OPEN')
def _get_active_games_with_player(self, player_nick):
"""Return the in progress/open games that include the player nick."""
games = []
db = self.get_db()
try:
# get the games of specified type and populate a list
cur = db.cursor(mdb.cursors.DictCursor)
query = '''
SELECT game.id, game.round_mode, game.game_length, game.line_length,
game.random_method, game.lines_per_turn, game.status, game.owner_nick,
game.owner_userhost, game.start_time, game.end_time
FROM storycraft_game game
INNER JOIN storycraft_player player ON player.game_id = game.id
WHERE player.nick = %s AND (game.status = 'OPEN' OR game.status = 'IN PROGRESS')
'''
cur.execute(query, (player_nick,))
results = cur.fetchall()
for result in results:
game = self.StorycraftGame()
game.id = int(result['id'])
game.round_mode = int(result['round_mode'])
game.game_length = int(result['game_length'])
game.line_length = int(result['line_length'])
game.random_method = int(result['random_method'])
game.lines_per_turn = int(result['lines_per_turn'])
game.status = result['status']
game.owner_nick = result['owner_nick']
game.owner_userhost = result['owner_userhost']
game.start_time = result['start_time'].replace(tzinfo=tzlocal())
game.end_time = result['end_time']
if game.end_time is not None:
game.end_time = game.end_time.replace(tzinfo=tzlocal())
games.append(game)
return games
except mdb.Error as e:
self.log.error("database error during get active games with player")
self.log.exception(e)
raise
finally: cur.close()
def _get_games_waiting_on_player(self, player_nick):
"""Return the games where the player nick is the owner of a pending line.
Include the games that the player started and are open, since they're waiting
on the person too.
"""
games = []
db = self.get_db()
try:
# get the games of specified type and populate a list
cur = db.cursor(mdb.cursors.DictCursor)
query = '''
SELECT game.id, game.round_mode, game.game_length, game.line_length,
game.random_method, game.lines_per_turn, game.status, game.owner_nick,
game.owner_userhost, game.start_time, game.end_time
FROM storycraft_game game
INNER JOIN storycraft_player player ON player.game_id = game.id
INNER JOIN storycraft_line line ON line.player_id = player.id
WHERE player.nick = %s AND game.status = 'IN PROGRESS' AND line.line = ''
UNION
SELECT game.id, game.round_mode, game.game_length, game.line_length,
game.random_method, game.lines_per_turn, game.status, game.owner_nick,
game.owner_userhost, game.start_time, game.end_time
FROM storycraft_game game
WHERE game.owner_nick = %s AND game.status = 'OPEN'
'''
cur.execute(query, (player_nick, player_nick))
results = cur.fetchall()
for result in results:
game = self.StorycraftGame()
game.id = int(result['id'])
game.round_mode = int(result['round_mode'])
game.game_length = int(result['game_length'])
game.line_length = int(result['line_length'])
game.random_method = int(result['random_method'])
game.lines_per_turn = int(result['lines_per_turn'])
game.status = result['status']
game.owner_nick = result['owner_nick']
game.owner_userhost = result['owner_userhost']
game.start_time = result['start_time'].replace(tzinfo=tzlocal())
game.end_time = result['end_time']
if game.end_time is not None:
game.end_time = game.end_time.replace(tzinfo=tzlocal())
games.append(game)
return games
except mdb.Error as e:
self.log.error("database error during get games waiting on player")
self.log.exception(e)
raise
finally: cur.close()
def _get_games_of_type(self, game_type):
"""Return the games of the specified type."""
games = []
db = self.get_db()
try:
# get the games of specified type and populate a list
cur = db.cursor(mdb.cursors.DictCursor)
query = '''
SELECT id, round_mode, game_length, line_length, random_method,
lines_per_turn, status, owner_nick, owner_userhost, start_time,
end_time
FROM storycraft_game WHERE status = %s
'''
cur.execute(query, (game_type,))
results = cur.fetchall()
for result in results:
game = self.StorycraftGame()
game.id = int(result['id'])
game.round_mode = int(result['round_mode'])
game.game_length = int(result['game_length'])
game.line_length = int(result['line_length'])
game.random_method = int(result['random_method'])
game.lines_per_turn = int(result['lines_per_turn'])
game.status = result['status']
game.owner_nick = result['owner_nick']
game.owner_userhost = result['owner_userhost']
game.start_time = result['start_time'].replace(tzinfo=tzlocal())
game.end_time = result['end_time']
if game.end_time is not None:
game.end_time = game.end_time.replace(tzinfo=tzlocal())
games.append(game)
return games
except mdb.Error as e:
self.log.error("database error during get games of type")
self.log.exception(e)
raise
finally: cur.close()
def _add_player_to_game(self, game_id, nick, userhost):
"""Add a player to a game, so that they may eventually play."""
db = self.get_db()
try:
cur = db.cursor(mdb.cursors.DictCursor)
statement = '''
INSERT INTO storycraft_player (game_id, nick, userhost)
VALUES (%s, %s, %s)
'''
cur.execute(statement, (game_id, nick, userhost))
db.commit()
return cur.lastrowid
except mdb.Error as e:
db.rollback()
self.log.error("database error during add player to game")
self.log.exception(e)
raise
finally: cur.close()
def _get_player_exists_in_game(self, game_id, userhost):
"""Return the existence of a player in a game."""
return self._get_player_by_userhost_in_game(game_id, userhost) is not None
def _get_completed_game_count(self):
"""Get the number of games with COMPLETED status."""
return self._get_game_type_count('COMPLETED')
def _get_in_progress_game_count(self):
"""Get the number of games with IN PROGRESS status."""
return self._get_game_type_count('IN PROGRESS')
def _get_open_game_count(self):
"""Get the number of games with OPEN status."""
return self._get_game_type_count('OPEN')
def _get_free_game_count(self):
"""Get the number of slots available, given current in progess/open games."""
all_slots = self._get_concurrent_game_count()
count_in_progress = self._get_in_progress_game_count()
count_open = self._get_open_game_count()
return all_slots - count_in_progress - count_open
def _get_game_type_count(self, game_type):
"""Return the number of games of the specified type."""
count = 0
db = self.get_db()
try:
# get count of game_type games
cur = db.cursor(mdb.cursors.DictCursor)
query = 'SELECT COUNT(*) FROM storycraft_game WHERE status = %s'
cur.execute(query, (game_type,))
result = cur.fetchone()
if result:
count = result['COUNT(*)']
except mdb.Error as e:
self.log.error("database error during get game type count")
self.log.exception(e)
raise
finally: cur.close()
return count
def _get_concurrent_game_count(self):
"""Return the current game server concurrency."""
concurrency = 0
db = self.get_db()
try:
# get the concurrency value from config table
cur = db.cursor(mdb.cursors.DictCursor)
query = 'SELECT concurrent_games FROM storycraft_config'
cur.execute(query)
result = cur.fetchone()
if result:
concurrency = result['concurrent_games']
except mdb.Error as e:
self.log.error("database error during get concurrent game count")
self.log.exception(e)
raise
finally: cur.close()
return concurrency
def _get_lines_for_game(self, game_id):
"""Get the lines for the specified game_id."""
lines = []
db = self.get_db()
try:
# get the games of specified type and populate a list
cur = db.cursor(mdb.cursors.DictCursor)
query = '''
SELECT id, game_id, player_id, line, time
FROM storycraft_line WHERE game_id = %s
ORDER BY time DESC, id DESC
'''
cur.execute(query, (game_id,))
results = cur.fetchall()
for result in results:
line = self.StorycraftLine()
line.id = result['id']
line.game_id = result['game_id']
line.player_id = result['player_id']
line.line = result['line']
line.time = result['time']
lines.append(line)
return lines
except mdb.Error as e:
self.log.error("database error during get lines for game")
self.log.exception(e)
raise
finally: cur.close()
def _update_line(self, line_id, input_line):
"""Update the specified line with the given text."""
db = self.get_db()
try:
cur = db.cursor(mdb.cursors.DictCursor)
statement = '''
UPDATE storycraft_line SET line = %s, time = CURRENT_TIMESTAMP WHERE id = %s
'''
cur.execute(statement, (input_line, line_id))
db.commit()
return line_id
except mdb.Error as e:
db.rollback()
self.log.error("database error during update line")
self.log.exception(e)
raise
finally: cur.close()
def _export_game_to_disk(self, game, lines):
"""Write a game to disk."""
filename = 'storycraft-{0:0000d}-{1:s}-{2:s}.txt'.format(game.id, game.owner_nick, time.strftime('%Y%m%d%H%M%S'))
f = open(filename, 'w')
lines_by_player = {}
line_count = 0
# the array is in LIFO, so reverse it
lines.reverse()
for line in lines:
# get player for this line
player = self._get_player_by_id(line.player_id)
line_count = line_count + 1
if not player.nick in lines_by_player:
lines_by_player[player.nick] = []
lines_by_player[player.nick].append(str(line_count))
f.write(line.line + '\n')
f.write('\n')
for player in lines_by_player.keys():
f.write(player + ':' + ','.join(lines_by_player[player]) + '\n')
f.close()
return filename
def _get_storycraft_settings(self):
"""Get the server settings."""
db = self.get_db()
try:
# get the settings and return in StorycraftSettings
cur = db.cursor(mdb.cursors.DictCursor)
query = '''
SELECT master_channel, concurrent_games, default_round_mode,
default_game_length, default_line_length, default_random_method,
default_lines_per_turn
FROM storycraft_config
'''
cur.execute(query)
result = cur.fetchone()
if result:
settings = self.StorycraftSettings()
settings.master_channel = result['master_channel']
settings.concurrent_games = int(result['concurrent_games'])
settings.default_round_mode = int(result['default_round_mode'])
settings.default_game_length = int(result['default_game_length'])
settings.default_line_length = int(result['default_line_length'])
settings.default_random_method = int(result['default_random_method'])
settings.default_lines_per_turn = int(result['default_lines_per_turn'])
return settings
except mdb.Error as e:
self.log.error("database error during get storycraft settings")
self.log.exception(e)
raise
finally: cur.close()
# vi:tabstop=4:expandtab:autoindent