2020-02-24 18:52:56 +00:00

624 lines
26 KiB
Python

###
# Copyright (c) SpiderDave
# Copyright (c) 2020, oddluck <oddluck@riseup.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import re
import random
import supybot.conf as conf
import supybot.utils as utils
from supybot.commands import *
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
import time
import os, errno
import pickle
# This will be used to change the name of the class to the folder name
PluginName=os.path.dirname( __file__ ).split(os.sep)[-1]
class _Plugin(callbacks.Plugin):
"""
Implementation of games (Undercut, Flaunt, SuperFlaunt) described
in Metamagical Themas by Douglas Hoffsteder.
"""
threaded = True
game=[{},{},{},{},{}]
channeloptions = {}
channeloptions['allow_game']=False
channeloptions['debug']=False
channeloptions['use_queue']=True
channeloptions['undercut_goal']=40
channeloptions['flaunt1_goal']=40
channeloptions['flaunt2_goal']=200
channeloptions['flaunt3_goal']=40
lastgame=time.time()
def make_sure_path_exists(path):
try:
os.makedirs(path)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise
make_sure_path_exists(r'%s%sundercut' % (conf.supybot.directories.data(),os.sep))
dataPath=r'%s%sundercut%s' % (conf.supybot.directories.data(),os.sep,os.sep)
prefixChar = conf.supybot.reply.whenAddressedBy.chars()[0]
def ucstart(self, irc, msg, args, text):
"""[<gametype>]
Start a new game of Undercut/Flaunt. For the rules of the game, use the ucrules command.
Valid game types are undercut, flaunt1, flaunt2, and flaunt3.
"""
try:
self._read_options(irc)
except:
pass
if self.channeloptions['allow_game']==False:
irc.reply('Error: allow_game=False')
return
if text:
gametype=text.lower().strip()
if gametype.replace(' ', '')=='globalthermalnuclearwar':
irc.reply('Curious game. The only winning move is not to play.')
return
if gametype not in ['undercut', 'flaunt1', 'flaunt2', 'flaunt3']:
irc.reply('Error: Invald game type %s.' % gametype)
return
else:
gametype='undercut'
nick=msg.nick
table=self._gettablefromnick(nick)
if table != None:
gametype=self.game[table].get('type').capitalize()
irc.reply('Error: You are already in a game of %s.' % gametype)
return
table=self._getopentable()
if table==None:
irc.reply('Sorry, all the game tables are in use at the moment.')
return
self._cleanup(table)
self.game[table]['channel']=msg.args[0]
self.game[table]['type']=gametype
goal=self.channeloptions[gametype+'_goal']
self.game[table]['goal']=goal
self.game[table]['players'][nick]={'score':0}
self.game[table]['players'][nick]['numbers']=[0]
irc.reply('%s has started a new game of %s at table %s. For the rules of the game, type ".ucrules". To accept this challenge, join with .ucjoin.' % (nick, gametype.capitalize(), table+1), prefixNick=False)
self.game[table]['phase']='join'
ucstart = wrap(ucstart, ['public', optional('something')])
def ucrules(self, irc, msg, args):
"""takes no arguments
Display rules for Undercut/Flaunt.
"""
irc.reply('Rules for Undercut/Flaunt: http://pastebin.com/raw.php?i=9cZ6ykWX Start a game with .ucstart <gametype>. Valid gametypes are undercut, flaunt1, flaunt2, and flaunt3.')
ucrules=wrap(ucrules)
def ucjoin(self, irc, msg, args, table, fakenick):
"""[<table>]
Join a game of Undercut/Flaunt previously started with the ucstart command.
Specify <table> if there is more than one game to join in that channel.
"""
try:
self._read_options(irc)
except:
pass
if self.channeloptions['allow_game']==False:
irc.reply('Error: allow_game=False')
return
nick=msg.nick
if table !=None: table-=1 # make tables zero based
tables=self._getcurrenttables()
if not tables:
# no games running
irc.reply('Error: There are no games to join.')
return
if table !=None and table not in tables:
# given table doesn't have a game going
if table not in list(range(len(self.game))):
irc.reply("Error: That table doesn't exist")
return
irc.reply("Error: There is no game at that table")
return
tables=[t for t in tables if self.game[t]['channel']==msg.args[0]]
if table !=None:
if table not in tables:
irc.reply('Error: That table is in another channel.')
return
tables=[table] # success!
if len(tables)==0:
irc.reply('Error: There are no games to join in this channel.')
return
elif len(tables)==1:
table=tables[0]
else:
messagetxt="Please specify which table you'd like to play at (ucjoin <table>). Current tables are: "
for t in tables:
messagetxt+='Table %s (%s), ' % (t+1, ' '.join(list(self.game[t]['players'].keys())))
messagetxt=messagetxt.rsplit(', ',1)[0]+'.'
irc.reply(messagetxt)
return
isfake=False
iscpu=False
if ((self.channeloptions['debug']) and fakenick) or (fakenick and fakenick.lower()=='cpu'):
nick=fakenick
isfake=True
if fakenick.lower()=='cpu': iscpu=True
if self.game[table]['phase']=='join':
if nick in list(self.game[table]['players'].keys()):
irc.reply('Error: you have already joined.')
return
self.game[table]['players'][nick]={'score':0}
self.game[table]['players'][nick]['numbers']=[0]
irc.reply('Game started! Use .ucplay (privately) to play a number from 1 to 5.', prefixNick=False, to=self.game[table]['channel'])
self.game[table]['phase']='running'
else:
if self.game[table]['phase']=='running':
irc.reply('Error: Game already running.')
return
elif self.game[table]['phase']=='':
irc.reply('Error: You need to create a game with .ucstart first.')
return
else:
# don't know when this would happen, but whatever
irc.reply('Error: not join phase.')
return
ucjoin = wrap(ucjoin, ['public', optional('int'), optional('something')])
def ucleave(self, irc, msg, args, fakenick):
"""takes no arguments
Leave a game of Undercut/Flaunt.
"""
try:
self._read_options(irc)
except:
pass
if self.channeloptions['allow_game']==False:
irc.reply('Error: allow_game=False')
return
nick=msg.nick
if self.channeloptions['debug'] and fakenick:
nick=fakenick
table=self._gettablefromnick(nick)
if table==None:
irc.reply('Error: You are not playing a game at any of the tables.')
return
irc.reply('%s has left the game.' % nick, prefixNick=False, to=self.game[table]['channel'])
del self.game[table]['players'][nick]
winner=[p for p in self.game[table]['players']]
if len(winner)>0:
winner=winner[0]
irc.reply('%s wins!' % winner, prefixNick=False, to=self.game[table]['channel'])
else:
irc.reply('The game has been cancelled.', prefixNick=False, to=self.game[table]['channel'])
self.game[table]['phase']='gameover'
self._cleanup(table)
ucleave = wrap(ucleave, ['public', optional('something')])
def _leavegame(self, irc, msg, nick):
"""takes no arguments
Leave a game of Undercut/Flaunt.
"""
try:
self._read_options(irc)
except:
pass
if self.channeloptions['allow_game']==False:
irc.reply('Error: allow_game=False')
return
table=self._gettablefromnick(nick)
if table==None:
#irc.reply('Error: You are not playing a game at any of the tables.')
return
#irc.reply('%s has left the game.' % nick, prefixNick=False, to=self.game[table]['channel'])
# ---- replace with cpu ----
# ** old uno specific stuff before I split it off;
# may want to adapt it for this **
# oldnick=nick
# nick=self._uno_make_cpu(table)
# del self.game[table]['players'][nick] # remove new cpu player (we just want the nick)
# self.game[table]['players'][nick]=self.game[table]['players'][oldnick]
# del self.game[table]['players'][oldnick]
# self.game[table]['players'][nick]['fake']=True
# self.game[table]['players'][nick]['cpu']=True
# irc.reply('%s has been replaced by %s.' % (oldnick, nick), prefixNick=False, to=self.game[table]['channel'])
# return
def ucplay(self, irc, msg, args, number, fakenick):
"""<number>
Play a <number> for the Undercut/Flaunt games. This command should
be used in a private message."""
nick=msg.nick
if self.channeloptions['debug'] and fakenick:
nick=fakenick
table=self._gettablefromnick(nick)
if table==None:
irc.reply('Error: You are not playing a game at any of the tables.')
return
if self.game[table]['phase']=='running':
if nick not in self.game[table]['players']:
irc.reply("Error: You're not playing this game.")
return
if number not in list(range(1,5+1)):
irc.reply('Error: You must play a number between 1 and 5.')
return
opponent=[p for p in self.game[table]['players'] if p !=nick][0]
if len(self.game[table]['players'][opponent]['numbers'])==len(self.game[table]['players'][nick]['numbers']):
self.game[table]['players'][nick]['numbers'].append(number)
irc.reply('%s made his move.' % nick, to=self.game[table]['channel'])
elif len(self.game[table]['players'][opponent]['numbers'])<len(self.game[table]['players'][nick]['numbers']):
irc.reply('Error: You must wait for your opponent.')
elif len(self.game[table]['players'][opponent]['numbers'])>len(self.game[table]['players'][nick]['numbers']):
self.game[table]['players'][nick]['numbers'].append(number)
irc.reply('%s made his move.' % (nick), to=self.game[table]['channel'])
players=[p for p in self.game[table]['players']]
numbers=[self.game[table]['players'][p]['numbers'][-1] for p in players]
point=[0,0]
flaunttxt=['','']
gametype=self.game[table]['type']
if gametype=='undercut':
if numbers[0]==numbers[1]-1:
undercut=0
point[0]=sum(numbers)
elif numbers[1]==numbers[0]-1:
undercut=1
point[1]=sum(numbers)
else:
undercut=None
point[0]=numbers[0]
point[1]=numbers[1]
elif gametype=='flaunt1':
flaunt=[0,0]
for p in range(len(players)):
for i in range(len(self.game[table]['players'][players[p]]['numbers'])-1):
if self.game[table]['players'][players[p]]['numbers'][-i-2] != self.game[table]['players'][players[p]]['numbers'][-i-1]:
flaunt[p]=i+1
break;
if numbers[0]==numbers[1]-1:
undercut=0
point[0]=sum(numbers)
point[0]=numbers[0]*flaunt[0]+numbers[1]*flaunt[1]
elif numbers[1]==numbers[0]-1:
undercut=1
point[1]=sum(numbers)
point[1]=numbers[1]*flaunt[1]+numbers[0]*flaunt[0]
else:
undercut=None
point[0]=numbers[0]*flaunt[0]
point[1]=numbers[1]*flaunt[1]
flaunttxt[0]='x%s' % flaunt[0]
flaunttxt[1]='x%s' % flaunt[1]
flaunttxt[0]=flaunttxt[0].replace('x1','')
flaunttxt[1]=flaunttxt[1].replace('x1','')
elif gametype=='flaunt2':
flaunt=[0,0]
for p in range(len(players)):
for i in range(len(self.game[table]['players'][players[p]]['numbers'])-1):
if self.game[table]['players'][players[p]]['numbers'][-i-2] != self.game[table]['players'][players[p]]['numbers'][-i-1]:
flaunt[p]=i+1
break;
if numbers[0]==numbers[1]-1:
undercut=0
point[0]=sum(numbers)
point[0]=numbers[0]**flaunt[0]+numbers[1]**flaunt[1]
elif numbers[1]==numbers[0]-1:
undercut=1
point[1]=sum(numbers)
point[1]=numbers[1]**flaunt[1]+numbers[0]**flaunt[0]
else:
undercut=None
point[0]=numbers[0]**flaunt[0]
point[1]=numbers[1]**flaunt[1]
flaunttxt[0]='^%s' % flaunt[0]
flaunttxt[1]='^%s' % flaunt[1]
flaunttxt[0]=flaunttxt[0].replace('^1','')
flaunttxt[1]=flaunttxt[1].replace('^1','')
elif gametype=='flaunt3':
flaunt=[0,0]
for p in range(len(players)):
for i in range(len(self.game[table]['players'][players[p]]['numbers'])-1):
if self.game[table]['players'][players[p]]['numbers'][-i-2] != self.game[table]['players'][players[p]]['numbers'][-i-1]:
flaunt[p]=i
break;
if numbers[0]==numbers[1]-1:
undercut=0
point[0]=sum(numbers)
point[0]=numbers[0]+flaunt[0]+numbers[1]+flaunt[1]
elif numbers[1]==numbers[0]-1:
undercut=1
point[1]=sum(numbers)
point[1]=numbers[1]+flaunt[1]+numbers[0]+flaunt[0]
else:
undercut=None
point[0]=numbers[0]+flaunt[0]
point[1]=numbers[1]+flaunt[1]
flaunttxt[0]='+%s' % flaunt[0]
flaunttxt[1]='+%s' % flaunt[1]
flaunttxt[0]=flaunttxt[0].replace('+0','')
flaunttxt[1]=flaunttxt[1].replace('+0','')
boldplayer=None
if point[0]>point[1]: boldplayer=0
if point[1]>point[0]: boldplayer=1
txt=''
for p in range(len(players)):
if p==boldplayer:
txt+='%s: ' % players[p]
if p==undercut:
txt+=ircutils.bold('%s%s (undercut!)' % (numbers[p], flaunttxt[p]))
else:
txt+=ircutils.bold('%s%s' % (numbers[p], flaunttxt[p]))
else:
txt+='%s: %s%s' % (players[p], numbers[p], flaunttxt[p])
if p < len(players)-1:
txt+=', '
messagetxt=txt
messagetxt+=' // %s points for %s, %s points for %s.' % (point[0],players[0],point[1],players[1])
for i in range(len(players)):
self.game[table]['players'][players[i]]['score']+=point[i]
scores=[self.game[table]['players'][p]['score'] for p in players]
messagetxt+=' Total: %s(%s), %s(%s).' % (players[0], scores[0], players[1], scores[1])
irc.reply(messagetxt, to=self.game[table]['channel'])
if scores[0]>=self.game[table]['goal'] and scores[0]==scores[1]:
# both >= 40, equal scores
irc.reply("The game is over. It's a tie!", to=self.game[table]['channel'])
self.game[table]['phase']='gameover'
elif scores[0]>=self.game[table]['goal'] and scores[1]>self.game[table]['goal']:
# both >= 40, but different scores
if scores[0]>scores[1]:
winner=players[0]
else:
winner=players[1]
irc.reply('The game is over. %s wins!' % winner, to=self.game[table]['channel'])
self.game[table]['phase']='gameover'
elif scores[0]>=self.game[table]['goal']:
# first player wins
irc.reply('The game is over. %s wins!' % players[0], to=self.game[table]['channel'])
self.game[table]['phase']='gameover'
elif scores[1]>=self.game[table]['goal']:
# second player wins
irc.reply('The game is over. %s wins!' % players[1], to=self.game[table]['channel'])
self.game[table]['phase']='gameover'
#irc.reply('%s' % self.game)
if self.game[table]['phase']=='gameover':
self._cleanup(table)
else:
irc.reply('Error: game not running')
ucplay = wrap(ucplay, ['private', 'int', optional('something')])
def ucsetoption(self, irc, msg, args, channel, text, value):
"""<option> <value>
Changes an option for the Undercut/Flaunt games. You can view the
options for the current channel with the ucshowoptions command."""
try:
self._read_options(irc)
except:
pass
if value.lower()=='true':
value=True
elif value.lower()=='false':
value=False
elif value.lower()=='unset':
if text in self.channeloptions:
irc.reply('Set %s %s-->(unset)' % (text, self.channeloptions[text]))
del self.channeloptions[text]
try:
self._write_options(irc)
except:
irc.reply('Failed to write options to file. :(')
else:
irc.reply('%s was already unset.' % text)
return
if text in self.channeloptions:
irc.reply('Set %s %s-->%s' % (text, self.channeloptions[text], value))
self.channeloptions[text]=value
else:
irc.reply('Set %s (unset)-->%s' % (text, value))
self.channeloptions[text]=value
try:
self._write_options(irc)
except:
irc.reply('Failed to write options to file. :(')
ucsetoption = wrap(ucsetoption, [('checkChannelCapability', 'op'), 'something', 'something'])
def ucshowoptions(self, irc, msg, args):
"""(takes no arguments)
Shows options for the Undercut/Flaunt games for the current channel."""
try:
self._read_options(irc)
except:
pass
txt=', '.join(['='.join([str(i) for i in item]) for item in list(self.channeloptions.items())])
irc.reply(txt)
ucshowoptions = wrap(ucshowoptions)
def _cleanup(self, table):
self.game[table]={}
self.game[table]['players']={}
self.game[table]['phase']=''
def _getopentable(self):
openslot=[i for i in range(len(self.game)) if not self.game[i].get('phase')]
if len(openslot)==0:
return None
else:
return openslot[0]
def _getcurrenttables(self):
slot=[i for i in range(len(self.game)) if self.game[i].get('phase')]
return slot
def _gettablefromnick(self, n):
tables=self._getcurrenttables()
if not tables: return None
for table in tables:
#if n.lower() in map(lambda x:x.lower(), self.game[table]['players'].keys()):
if n.lower() in [x.lower() for x in list(self.game[table]['players'].keys())]:
return table
return None
def _read_options(self, irc):
network=irc.network.replace(' ','_')
channel=irc.msg.args[0]
#irc.reply('test: %s.%s.options' % (irc.network, irc.msg.args[0] ))
f="%s%s.%s.options" % (self.dataPath, network, channel)
if os.path.isfile(f):
inputfile = open(f, "rb")
self.channeloptions = pickle.load(inputfile)
inputfile.close()
else:
# Use defaults
channeloptions = {}
channeloptions['allow_game']=False
channeloptions['debug']=False
channeloptions['use_queue']=True
channeloptions['undercut_goal']=40
channeloptions['flaunt1_goal']=40
channeloptions['flaunt2_goal']=200
channeloptions['flaunt3_goal']=40
return
def _write_options(self, irc):
network=irc.network.replace(' ','_')
channel=irc.msg.args[0]
outputfile = open("%s%s.%s.options" % (self.dataPath, network, channel), "wb")
pickle.dump(self.channeloptions, outputfile)
outputfile.close()
# def _get_default_options(self):
# self.channeloptions = {}
# self.channeloptions['allow_game']=False
# self.channeloptions['debug']=False
def doNick(self, irc, msg):
oldNick = msg.nick
newNick = msg.args[0]
table=self._gettablefromnick(oldNick)
if table == None:
return
self.game[table]['players'][newNick]=self.game[table]['players'][oldNick]
del self.game[table]['players'][oldNick]
def doQuit(self, irc, msg):
nick=msg.nick
table=self._gettablefromnick(nick)
if table == None:
return
self._leavegame(irc, msg, nick)
def doPart(self, irc, msg):
#self.log.info('doPart debug: msg.args[0]=%s, msg.args[1]=%s, msg.command=%s, msg.nick=%s' % (msg.args[0], msg.args[1], msg.command, msg.nick))
nick=msg.nick
table=self._gettablefromnick(nick)
if table == None:
return
if msg.args[0] == self.game[table]['channel']:
self._leavegame(irc, msg, nick)
def doKick(self, irc, msg):
(channel, nicks) = msg.args[:2]
nicks=nicks.split(',')
for nick in nicks:
table=self._gettablefromnick(nick)
if table!=None:
self._leavegame(irc, msg, nick)
def _sendMsg(self, irc, msg):
if self.channeloptions['use_queue']:
irc.queueMsg(msg)
else:
irc.sendMsg(msg)
irc.noReply()
def reply(self, irc, text, action=False, private=False, prefixNick=False, to='', fast=False):
table=self._gettablefromnick(to)
if table == None:
# hopefully it's a valid channel
pass
else:
if self.game[table]['players'][to].get('fake'):
if self.channeloptions['debug']:
text='(to %s): %s' % (to, text)
text=ircutils.mircColor(text, fg=14)
to=self.game[table]['channel']
else:
# No need to show cpu actions anywhere if debug is false.
return
if action==True or fast==False:
irc.reply(text, action=action, private=private, prefixNick=prefixNick, to=to)
else:
if (prefixNick) and ('#' not in to):
text='%s: %s' % (to, text)
m=ircmsgs.privmsg(to, text)
self._sendMsg(irc, m)
_Plugin.__name__=PluginName
Class = _Plugin
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: