another "this is unnecessary" change, obviously impacting all the modules that override __init__ as well as the base class. again, they can use the DrBotIRC instance for anything, which is (with one exception) only for add/remove_global_handler, which i'm planning on working my way off of anyway
1188 lines
51 KiB
Python
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
|