diff --git a/dr_botzo/dr_botzo/settings.py b/dr_botzo/dr_botzo/settings.py index 6999a62..4ed5d00 100644 --- a/dr_botzo/dr_botzo/settings.py +++ b/dr_botzo/dr_botzo/settings.py @@ -47,6 +47,7 @@ INSTALLED_APPS = ( 'pi', 'races', 'seen', + 'storycraft', 'twitter', ) @@ -156,6 +157,14 @@ IRCBOT_XMLRPC_PORT = 13132 # IRC module stuff +# storycraft + +STORYCRAFT_MASTER_CHANNEL = '#dr.botzo' +STORYCRAFT_CONCURRENT_GAMES = 10 +STORYCRAFT_DEFAULT_GAME_LENGTH = 20 +STORYCRAFT_DEFAULT_LINE_LENGTH = 140 +STORYCRAFT_DEFAULT_LINES_PER_TURN = 2 + # twitter TWITTER_CONSUMER_KEY = None diff --git a/dr_botzo/storycraft/__init__.py b/dr_botzo/storycraft/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dr_botzo/storycraft/admin.py b/dr_botzo/storycraft/admin.py new file mode 100644 index 0000000..adbe09d --- /dev/null +++ b/dr_botzo/storycraft/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from storycraft.models import StorycraftGame, StorycraftPlayer, StorycraftLine + + +admin.site.register(StorycraftGame, admin.ModelAdmin) +admin.site.register(StorycraftPlayer, admin.ModelAdmin) +admin.site.register(StorycraftLine, admin.ModelAdmin) diff --git a/dr_botzo/storycraft/ircplugin.py b/dr_botzo/storycraft/ircplugin.py new file mode 100644 index 0000000..e51e01d --- /dev/null +++ b/dr_botzo/storycraft/ircplugin.py @@ -0,0 +1,568 @@ +"""Collaborative nonsense story writing.""" + +from __future__ import unicode_literals + +import logging +import random +import re +import time + +from django.conf import settings +from django.utils import timezone +from irc.client import NickMask + +from ircbot.lib import Plugin +from storycraft.models import StorycraftGame, StorycraftPlayer, StorycraftLine + + +log = logging.getLogger('storycraft.ircplugin') + + +class Storycraft(Plugin): + + """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. + """ + + def start(self): + """Hook handler functions into the IRC library.""" + + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!storycraft\s+status$', + self.handle_status) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], + r'^!storycraft\s+new\s+game(\s+with\s+config\s+(.*)$|$)', + self.handle_new_game) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], + r'^!storycraft\s+game\s+(\d+)\s+join$', + self.handle_join_game) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], + r'^!storycraft\s+list\s+games\s+(open|in progress|completed|my games|waiting for me)$', + self.handle_list_games) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], + r'^!storycraft\s+game\s+(\d+)\s+start$', + self.handle_start_game) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], + r'^!storycraft\s+game\s+(\d+)\s+show\s+line$', + self.handle_show_line) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], + r'^!storycraft\s+game\s+(\d+)\s+add\s+line\s+(.*)$', + self.handle_add_line) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], + r'^!storycraft\s+game\s+(\d+)\s+status$', + self.handle_game_status) + + super(Storycraft, self).start() + + def stop(self): + """Unhook handler functions into the IRC library.""" + + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_status) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_new_game) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_join_game) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_list_games) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_start_game) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_show_line) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_add_line) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_game_status) + + super(Storycraft, self).stop() + + def handle_status(self, connection, event, match): + """Print information about the storycraft games, or one specific game.""" + + # do the general status of all games + count_completed = self._get_completed_games().count() + count_in_progress = self._get_in_progress_games().count() + count_open = self._get_open_games().count() + count_free = self._get_free_game_count() + + return self.bot.reply(event, "Storycraft - {0:d} games completed, {1:d} in progress, {2:d} open. " + "{3:d} slots free.".format(count_completed, count_in_progress, count_open, count_free)) + + def handle_game_status(self, connection, event, match): + """Print information about one specific game.""" + + 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 = game.summary() + + return self.bot.reply(event, status_str) + else: + return self.bot.reply(event, "Game #{0:d} does not exist.".format(game_id)) + + def handle_new_game(self, connection, event, match): + """Add a game to the system, which users can join.""" + + nick = NickMask(event.source).nick + + # ensure the system isn't at capacity + count_free = self._get_free_game_count() + if count_free > 0: + # get the default settings + master_channel = settings.STORYCRAFT_MASTER_CHANNEL + game_length = settings.STORYCRAFT_DEFAULT_GAME_LENGTH + line_length = settings.STORYCRAFT_DEFAULT_LINE_LENGTH + lines_per_turn = settings.STORYCRAFT_DEFAULT_LINES_PER_TURN + + options = match.group(2) + if options: + # override settings with provided options + tuples = options.split(';') + for option in tuples: + optpair = option.split(',') + + if len(optpair) == 2: + if optpair[0] == 'game_length': + game_length = int(optpair[1]) + elif optpair[0] == 'line_length': + line_length = int(optpair[1]) + elif optpair[0] == 'lines_per_turn': + lines_per_turn = int(optpair[1]) + + # add a new game + game = self._add_new_game(game_length, line_length, lines_per_turn, nick, event.source) + if game: + # add the player to the game, too + player = StorycraftPlayer.objects.create(nick=nick, nickmask=event.source, game=game) + + # tell the control channel + self.bot.privmsg(master_channel, "{0:s} created a game of storycraft - do '!storycraft game " + "{1:d} join' to take part!".format(nick, game.pk)) + + log.debug("%s added a new game", nick) + return self.bot.reply(event, "Game #{0:d} has been created. When everyone has joined, do " + "'!storycraft game {0:d} start'".format(game.pk)) + else: + return self.bot.reply(event, "Error creating game.") + else: + return self.bot.reply(event, "All slots are full.") + + def handle_join_game(self, connection, event, match): + """Add a player to an open game.""" + + nick = NickMask(event.source).nick + 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 == StorycraftGame.STATUS_OPEN: + # see if userhost is already in the game + if not self._get_player_exists_in_game(game_id, event.source): + # add the player + player = StorycraftPlayer.objects.create(nick=nick, nickmask=event.source, game=game) + if player: + # output results + master_channel = settings.STORYCRAFT_MASTER_CHANNEL + + self.bot.privmsg(master_channel, "{0:s} joined storycraft #{1:d}!".format(nick, game_id)) + log.debug("%s joined game #%d", nick, game_id) + return self.bot.reply(event, "{0:s}, welcome to the game.".format(nick)) + else: + return self.bot.reply(event, "Error joining game.") + else: + return self.bot.reply(event, "You are already in game #{0:d}.".format(game_id)) + else: + return self.bot.reply(event, "Game #{0:d} is not open for new players.".format(game_id)) + else: + return self.bot.reply("Game #{0:d} does not exist.".format(game_id)) + + def handle_list_games(self, connection, event, match): + """Get the listing of either open or in progress games.""" + + nick = NickMask(event.source).nick + category = match.group(1) + + games = list() + 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 self.bot.reply(event, "Too many to list! ids: {0:s}".format(','.join(gameids))) + else: + # show game details, since there's not many + gamestrs = [] + for game in games: + gamestrs.append(game.summary()) + + return self.bot.reply(event, "\n".join(gamestrs)) + + def handle_start_game(self, connection, event, match): + """Start a game, closing the period to join and starting line trading.""" + + nick = NickMask(event.source).nick + 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 event.source == game.owner_nickmask: + # check that the game is open + if game.status == StorycraftGame.STATUS_OPEN: + # start the game + game.status = StorycraftGame.STATUS_IN_PROGRESS + game.clean() + game.save() + + # determine the first person to send a line + if self._pick_next_player(game_id): + # indicate the status + line = game.get_lines()[0] + player = self._get_player_by_id(line.player_id) + + master_channel = settings.STORYCRAFT_MASTER_CHANNEL + + # tell the control channel + self.bot.privmsg(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)) + log.debug("%s started game #%d", nick, game_id) + + return self.bot.reply(event, "Game #{0:d} started.".format(game_id)) + else: + return self.bot.reply(event, "Failed to assign game to first player.") + else: + return self.bot.reply("Game #{0:d} is not open.".format(game_id)) + else: + return self.bot.reply(event, "You are not the owner of #{0:d}.".format(game_id)) + else: + return self.bot.reply(event, "Game #{0:d} does not exist.".format(game_id)) + + def handle_show_line(self, connection, event, match): + """List the line to continue with, if queryer is the assignee.""" + + 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 game.status == StorycraftGame.STATUS_IN_PROGRESS: + # get the most recent line + lines = game.get_lines() + 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.nickmask == event.source: + # check previous line + if len(lines) > 1: + # get previous line and display it + repline = lines[1] + return self.bot.reply(event, "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 self.bot.reply(event, "You are starting the story! Add the first line " + "with '!storycraft game {0:d} add line YOUR TEXT'." + "".format(game_id)) + else: + return self.bot.reply(event, "You are not the assignee of the current game.") + else: + return self.bot.reply(event, "Game is in progress but the most recent line is not " + "available - internal consistency error.") + else: + return self.bot.reply(event, "Game #{0:d} has no lines - internal consistency error." + "".format(game_id)) + else: + return self.bot.reply(event, "Game #{0:d} has not been started.".format(game_id)) + else: + return self.bot.reply(event, "Game #{0:d} does not exist.".format(game_id)) + + def handle_add_line(self, connection, event, match): + """Add a line to an in progress game.""" + + nick = NickMask(event.source).nick + 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 game.status == StorycraftGame.STATUS_IN_PROGRESS: + # get the most recent line + lines = game.get_lines() + 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.nickmask == event.source: + # check the input line length + if len(input_line) <= game.line_length: + # add line + line.line = input_line + line.clean() + line.save() + + # 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 = game.get_lines()[0] + last_line = game.get_lines()[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 = game.get_progress_string() + + # 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.bot.privmsg(player.nick, "You have a new line in storycraft " + "#{0:d}: '{1:s}' {2:s}" + "".format(game_id, last_line.line, progress_str)) + + master_channel = settings.STORYCRAFT_MASTER_CHANNEL + + log.debug("%s added a line to #%d", nick, game_id) + # log output + if game.status == StorycraftGame.STATUS_IN_PROGRESS: + self.bot.privmsg(master_channel, "{0:s} added a line to storycraft " + "#{1:d}! - next player is {2:s}" + "".format(nick, game_id, player.nick)) + return self.bot.reply(event, return_msg) + else: + self.bot.privmsg(master_channel, "{0:s} finished storycraft #{1:d}!" + "".format(nick, game_id)) + return self.bot.reply(event, "Line logged (and game completed).") + else: + return self.bot.reply(event, "Failed to assign game to next player.") + else: + return self.bot.reply(event, "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 self.bot.reply(event, "You are not the assignee of the current game.") + else: + return self.bot.reply(event, "Game is in progress but the most recent line is not " + "available - internal consistency error.") + else: + return self.bot.reply(event, "Game #{0:d} has no lines - internal consistency error." + "".format(game_id)) + else: + return self.bot.reply(event, "Game #{0:d} has not been started.".format(game_id)) + else: + return self.bot.reply(event, "Game #{0:d} does not exist.".format(game_id)) + + @staticmethod + def _get_game_details(game_id): + """Get the details of one game.""" + + try: + game = StorycraftGame.objects.get(pk=game_id) + return game + except StorycraftGame.DoesNotExist: + return None + + @staticmethod + def _add_new_game(game_length, line_length, lines_per_turn, nick, nickmask): + """Add a new game to the system.""" + + game = StorycraftGame.objects.create(game_length=game_length, line_length=line_length, + lines_per_turn=lines_per_turn, owner_nick=nick, + owner_nickmask=nickmask) + return game + + @staticmethod + def _get_player_list_for_game(game_id): + """Get the list of players in one game.""" + + try: + game = StorycraftGame.objects.get(pk=game_id) + players = StorycraftPlayer.objects.filter(game=game) + return players + except StorycraftGame.DoesNotExist: + return [] + + def _get_game_exists(self, game_id): + """Return the existence of a game.""" + + return self._get_game_details(game_id) is not None + + 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: + count_by_latest_player = 0 + latest_player_id = 0 + + # get the lines in this game, latest first + lines = game.get_lines() + + 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 += 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 + game.status = StorycraftGame.STATUS_COMPLETED + game.clean() + game.save() + return game + 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) + + @staticmethod + def _assign_game_to_player(game_id, player_id): + """Assign the game to a player, prompting them for responses.""" + + try: + game = StorycraftGame.objects.get(pk=game_id) + except StorycraftGame.DoesNotExist: + return None + + try: + player = StorycraftPlayer.objects.get(pk=player_id) + except StorycraftPlayer.DoesNotExist: + return None + + line = StorycraftLine.objects.create(game=game, player=player) + return line + + @staticmethod + def _get_player_by_id(player_id): + """Get the player details based on id.""" + + try: + player = StorycraftPlayer.objects.get(pk=player_id) + return player + except StorycraftPlayer.DoesNotExist: + return None + + @staticmethod + def _get_player_by_userhost_in_game(game_id, nickmask): + """Get the player details if they exist in the given game.""" + + try: + game = StorycraftGame.objects.get(pk=game_id) + except StorycraftGame.DoesNotExist: + return None + + players = StorycraftPlayer.objects.filter(nickmask=nickmask, game=game) + if len(players) == 1: + return players[0] + else: + return None + + def _get_completed_games(self): + """Get the games with COMPLETED status.""" + + return self._get_games_of_type(StorycraftGame.STATUS_COMPLETED) + + def _get_in_progress_games(self): + """Get the games with IN PROGRESS status.""" + + return self._get_games_of_type(StorycraftGame.STATUS_IN_PROGRESS) + + def _get_open_games(self): + """Get the games with OPEN status.""" + + return self._get_games_of_type(StorycraftGame.STATUS_OPEN) + + @staticmethod + def _get_active_games_with_player(player_nick): + """Return the in progress/open games that include the player nick.""" + + players = StorycraftPlayer.objects.filter(nick=player_nick) + game_ids = [x.game.pk for x in players] + + return StorycraftGame.objects.filter(pk__in=game_ids).filter(status__in=[StorycraftGame.STATUS_OPEN, + StorycraftGame.STATUS_IN_PROGRESS]) + + @staticmethod + def _get_games_waiting_on_player(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. + """ + + try: + player = StorycraftPlayer.objects.get(nick=player_nick) + except StorycraftPlayer.DoesNotExist: + return StorycraftGame.objects.none() + + latest_lines = StorycraftLine.objects.filter(player=player).latest('time') + return [x.game for x in latest_lines if x.line == ''] + + @staticmethod + def _get_games_of_type(status): + """Return the games of the specified type.""" + + return StorycraftGame.objects.filter(status=status) + + def _get_player_exists_in_game(self, game_id, nickmask): + """Return the existence of a player in a game.""" + + return self._get_player_by_userhost_in_game(game_id, nickmask) is not None + + def _get_free_game_count(self): + """Get the number of slots available, given current in progess/open games.""" + + all_slots = settings.STORYCRAFT_CONCURRENT_GAMES + count_in_progress = self._get_in_progress_games().count() + count_open = self._get_open_games().count() + return all_slots - count_in_progress - count_open + + +plugin = Storycraft diff --git a/dr_botzo/storycraft/migrations/0001_initial.py b/dr_botzo/storycraft/migrations/0001_initial.py new file mode 100644 index 0000000..58a79ea --- /dev/null +++ b/dr_botzo/storycraft/migrations/0001_initial.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='StorycraftGame', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('game_length', models.PositiveSmallIntegerField()), + ('line_length', models.PositiveSmallIntegerField()), + ('lines_per_turn', models.PositiveSmallIntegerField()), + ('status', models.CharField(max_length=16, choices=[('OPEN', 'OPEN'), ('IN PROGRESS', 'IN PROGRESS'), ('COMPLETED', 'COMPLETED')])), + ('owner_nick', models.CharField(max_length=64)), + ('owner_nickmask', models.CharField(max_length=200)), + ('start_time', models.DateTimeField()), + ('end_time', models.DateTimeField()), + ], + ), + migrations.CreateModel( + name='StorycraftLine', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('line', models.TextField(default='')), + ('time', models.DateTimeField(auto_now_add=True)), + ('game', models.ForeignKey(to='storycraft.StorycraftGame')), + ], + ), + migrations.CreateModel( + name='StorycraftPlayer', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('nick', models.CharField(max_length=64)), + ('nickmask', models.CharField(max_length=200)), + ('game', models.ForeignKey(to='storycraft.StorycraftGame')), + ], + ), + migrations.AddField( + model_name='storycraftline', + name='player', + field=models.ForeignKey(to='storycraft.StorycraftPlayer'), + ), + ] diff --git a/dr_botzo/storycraft/migrations/0002_auto_20150619_2034.py b/dr_botzo/storycraft/migrations/0002_auto_20150619_2034.py new file mode 100644 index 0000000..9452fad --- /dev/null +++ b/dr_botzo/storycraft/migrations/0002_auto_20150619_2034.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('storycraft', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='storycraftgame', + name='end_time', + field=models.DateTimeField(null=True), + ), + migrations.AlterField( + model_name='storycraftgame', + name='start_time', + field=models.DateTimeField(null=True), + ), + ] diff --git a/dr_botzo/storycraft/migrations/0003_auto_20150619_2038.py b/dr_botzo/storycraft/migrations/0003_auto_20150619_2038.py new file mode 100644 index 0000000..9969852 --- /dev/null +++ b/dr_botzo/storycraft/migrations/0003_auto_20150619_2038.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('storycraft', '0002_auto_20150619_2034'), + ] + + operations = [ + migrations.AlterField( + model_name='storycraftgame', + name='status', + field=models.CharField(default='OPEN', max_length=16, choices=[('OPEN', 'OPEN'), ('IN PROGRESS', 'IN PROGRESS'), ('COMPLETED', 'COMPLETED')]), + ), + ] diff --git a/dr_botzo/storycraft/migrations/0004_auto_20150619_2040.py b/dr_botzo/storycraft/migrations/0004_auto_20150619_2040.py new file mode 100644 index 0000000..c62ef4a --- /dev/null +++ b/dr_botzo/storycraft/migrations/0004_auto_20150619_2040.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('storycraft', '0003_auto_20150619_2038'), + ] + + operations = [ + migrations.AlterField( + model_name='storycraftline', + name='game', + field=models.ForeignKey(related_name='lines', to='storycraft.StorycraftGame'), + ), + migrations.AlterField( + model_name='storycraftline', + name='player', + field=models.ForeignKey(related_name='lines', to='storycraft.StorycraftPlayer'), + ), + migrations.AlterField( + model_name='storycraftplayer', + name='game', + field=models.ForeignKey(related_name='players', to='storycraft.StorycraftGame'), + ), + ] diff --git a/dr_botzo/storycraft/migrations/0005_storycraftgame_create_time.py b/dr_botzo/storycraft/migrations/0005_storycraftgame_create_time.py new file mode 100644 index 0000000..509bd9e --- /dev/null +++ b/dr_botzo/storycraft/migrations/0005_storycraftgame_create_time.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import datetime +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('storycraft', '0004_auto_20150619_2040'), + ] + + operations = [ + migrations.AddField( + model_name='storycraftgame', + name='create_time', + field=models.DateTimeField(default=datetime.datetime(2015, 6, 20, 1, 51, 18, 778824, tzinfo=utc), auto_now_add=True), + preserve_default=False, + ), + ] diff --git a/dr_botzo/storycraft/migrations/__init__.py b/dr_botzo/storycraft/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dr_botzo/storycraft/models.py b/dr_botzo/storycraft/models.py new file mode 100644 index 0000000..013c31f --- /dev/null +++ b/dr_botzo/storycraft/models.py @@ -0,0 +1,114 @@ +"""Track storycraft games.""" + +from __future__ import unicode_literals + +import logging + +from django.db import models +from django.utils import timezone + + +log = logging.getLogger('storycraft.models') + + +class StorycraftGame(models.Model): + + """Contain entire games of storycraft.""" + + STATUS_OPEN = 'OPEN' + STATUS_IN_PROGRESS = 'IN PROGRESS' + STATUS_COMPLETED = 'COMPLETED' + + STATUS_CHOICES = ( + (STATUS_OPEN, "OPEN"), + (STATUS_IN_PROGRESS, "IN PROGRESS"), + (STATUS_COMPLETED, "COMPLETED"), + ) + + game_length = models.PositiveSmallIntegerField() + line_length = models.PositiveSmallIntegerField() + lines_per_turn = models.PositiveSmallIntegerField() + status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=STATUS_OPEN) + owner_nick = models.CharField(max_length=64) + owner_nickmask = models.CharField(max_length=200) + + create_time = models.DateTimeField(auto_now_add=True) + start_time = models.DateTimeField(null=True) + end_time = models.DateTimeField(null=True) + + def __unicode__(self): + """String representation.""" + + return "Storycraft game {0:d}: {1:s}; {2:s}".format(self.pk, self.summary(), self.get_progress_string()) + + def get_progress_string(self): + """Get a terse summary of the game's progress.""" + + lines = self.get_lines() + num_lines = len(lines)-1 + + if num_lines == self.game_length - 1: + last_line = ' LAST LINE' + else: + last_line = '' + + progress = "lines: {0:d}/{1:d}{2:s}".format(num_lines, self.game_length, last_line) + return progress + + def get_lines(self): + """Get the lines for the game.""" + + return self.lines.order_by('-time') + + def summary(self): + """Display game info for a general summary.""" + + status_str = "#{0:d} - created on {1:s} by {2:s}, {3:s}".format(self.id, + timezone.localtime(self.create_time).strftime('%Y-%m-%d %H:%M:%S %Z'), + self.owner_nick, self.status) + if self.status == StorycraftGame.STATUS_COMPLETED and self.end_time: + status_str = status_str + ' ({0:s})'.format(self.end_time.strftime('%Y/%m/%d %H:%M:%S')) + elif self.status == StorycraftGame.STATUS_IN_PROGRESS: + lines = self.get_lines() + 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 = [] + for player in self.players.all(): + 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}]".format(self.game_length, self.line_length, + self.lines_per_turn) + + return status_str + + +class StorycraftPlayer(models.Model): + + """Contain entire games of storycraft.""" + + game = models.ForeignKey('StorycraftGame', related_name='players') + nick = models.CharField(max_length=64) + nickmask = models.CharField(max_length=200) + + def __unicode__(self): + """String representation.""" + + return "{0:s} in storycraft game {1:d}".format(self.nick, self.game.pk) + + +class StorycraftLine(models.Model): + + """Handle requests to dispatchers and do something with them.""" + + game = models.ForeignKey('StorycraftGame', related_name='lines') + player = models.ForeignKey('StorycraftPlayer', related_name='lines') + line = models.TextField(default="") + time = models.DateTimeField(auto_now_add=True) + + def __unicode__(self): + """String representation.""" + + return "line by {0:s} in storycraft game {1:d}".format(self.player.nick, self.game.pk)