storycraft: port to ircbot v2
this silly storytelling game has been lightly tested, but it appears to not blow up miserably, so i'm going to commit it until i can spend more time testing it/caring about it
This commit is contained in:
parent
095221a7b2
commit
1193ce5b6a
@ -47,6 +47,7 @@ INSTALLED_APPS = (
|
|||||||
'pi',
|
'pi',
|
||||||
'races',
|
'races',
|
||||||
'seen',
|
'seen',
|
||||||
|
'storycraft',
|
||||||
'twitter',
|
'twitter',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -156,6 +157,14 @@ IRCBOT_XMLRPC_PORT = 13132
|
|||||||
|
|
||||||
# IRC module stuff
|
# 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
|
||||||
|
|
||||||
TWITTER_CONSUMER_KEY = None
|
TWITTER_CONSUMER_KEY = None
|
||||||
|
0
dr_botzo/storycraft/__init__.py
Normal file
0
dr_botzo/storycraft/__init__.py
Normal file
8
dr_botzo/storycraft/admin.py
Normal file
8
dr_botzo/storycraft/admin.py
Normal file
@ -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)
|
568
dr_botzo/storycraft/ircplugin.py
Normal file
568
dr_botzo/storycraft/ircplugin.py
Normal file
@ -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
|
50
dr_botzo/storycraft/migrations/0001_initial.py
Normal file
50
dr_botzo/storycraft/migrations/0001_initial.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
24
dr_botzo/storycraft/migrations/0002_auto_20150619_2034.py
Normal file
24
dr_botzo/storycraft/migrations/0002_auto_20150619_2034.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
19
dr_botzo/storycraft/migrations/0003_auto_20150619_2038.py
Normal file
19
dr_botzo/storycraft/migrations/0003_auto_20150619_2038.py
Normal file
@ -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')]),
|
||||||
|
),
|
||||||
|
]
|
29
dr_botzo/storycraft/migrations/0004_auto_20150619_2040.py
Normal file
29
dr_botzo/storycraft/migrations/0004_auto_20150619_2040.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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,
|
||||||
|
),
|
||||||
|
]
|
0
dr_botzo/storycraft/migrations/__init__.py
Normal file
0
dr_botzo/storycraft/migrations/__init__.py
Normal file
114
dr_botzo/storycraft/models.py
Normal file
114
dr_botzo/storycraft/models.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user