""" 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 . """ 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(event, nick, userhost, what, admin_unlocked)) elif self.newgamere.search(what): return self.irc.reply(event, self.storycraft_newgame(event, nick, userhost, what, admin_unlocked)) elif self.joingamere.search(what): return self.irc.reply(event, self.storycraft_joingame(event, nick, userhost, what, admin_unlocked)) elif self.listgamesre.search(what): return self.irc.reply(event, self.storycraft_listgames(event, nick, userhost, what, admin_unlocked)) elif self.startgamere.search(what): return self.irc.reply(event, self.storycraft_startgame(event, nick, userhost, what, admin_unlocked)) elif self.showlinere.search(what): return self.irc.reply(event, self.storycraft_showline(event, nick, userhost, what, admin_unlocked)) elif self.addlinere.search(what): return self.irc.reply(event, self.storycraft_addline(event, nick, userhost, what, admin_unlocked)) elif self.gamestatusre.search(what): return self.irc.reply(event, self.storycraft_gamestatus(event, nick, userhost, what, admin_unlocked)) elif self.exportre.search(what): return self.irc.reply(event, self.storycraft_export(event, nick, userhost, what, admin_unlocked)) def storycraft_status(self, 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, 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, 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(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, 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(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, 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, 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(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, 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, 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(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(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(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, 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