Add unoplayer module

This commit is contained in:
dave 2017-01-01 14:01:23 -08:00
parent 9aff4f3f2b
commit afd31400c6
5 changed files with 375 additions and 24 deletions

View File

@ -0,0 +1,15 @@
{
"unobot": "BTN-Uno",
"unochannel": "#BTN-uno",
"streak_max": 3,
"enable_autojoin": true,
"enable_trigger": false,
"enable_delays": true,
"enable_randomhuman": true,
"randomhuman_chance": 15,
"randomhuman_sleep": 4,
"delay_joingame": [1.2, 12.0],
"delay_beforepickcolor": [1.0, 4.0],
"delay_beforedraw": [1.0, 3.0],
"delay_beforemove": [1.0, 4.0]
}

View File

@ -10,59 +10,60 @@ import logging
import os
from .pyircbot import PyIRCBot
class ModuleBase:
"""All modules will extend this class
:param bot: A reference to the main bot passed when this module is created
:type bot: PyIRCBot
:param moduleName: The name assigned to this module
:type moduleName: str
"""
def __init__(self, bot, moduleName):
self.moduleName=moduleName
"""Assigned name of this module"""
self.bot = bot
"""Reference to the master PyIRCBot object"""
self.hooks=[]
"""Hooks (aka listeners) this module has"""
self.services=[]
"""If this module provides services usable by another module, they're listed
here"""
self.config={}
"""Configuration dictionary. Autoloaded from `%(datadir)s/%(modulename)s.json`"""
self.log = logging.getLogger("Module.%s" % self.moduleName)
"""Logger object for this module"""
# Autoload config if available
self.loadConfig()
self.log.info("Loaded module %s" % self.moduleName)
def loadConfig(self):
"""Loads this module's config into self.config"""
configPath = self.bot.getConfigPath(self.moduleName)
if not configPath == None:
configPath = self.getConfigPath()
if configPath is not None:
self.config = PyIRCBot.load(configPath)
def ondisable(self):
"""Called when the module should be disabled. Your module should do any sort
of clean-up operations here like ending child threads or saving data files.
"""
pass
def getConfigPath(self):
"""Returns the absolute path of this module's json config file"""
return self.bot.getConfigPath(self.moduleName)
def getFilePath(self, f=None):
"""Returns the absolute path to a file in this Module's data dir
:param f: The file name included in the path
:type channel: str
:Warning: .. Warning:: this does no error checking if the file exists or is\

View File

@ -13,15 +13,15 @@ from time import sleep
class Services(ModuleBase):
def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName)
self.hooks=[ModuleHook("_CONNECT", self.doConnect), ModuleHook("433", self.nickTaken), ModuleHook("001", self.initservices), ModuleHook("INVITE", self.invited), ]
self.loadConfig()
self.hooks = [ModuleHook("_CONNECT", self.doConnect), ModuleHook("433", self.nickTaken), ModuleHook("001", self.initservices), ModuleHook("INVITE", self.invited), ]
self.current_nick = 0
self.do_ghost = False
def doConnect(self, args, prefix, trailing):
"""Hook for when the IRC conneciton is opened"""
self.bot.act_NICK(self.config["user"]["nick"][0])
self.bot.act_USER(self.config["user"]["username"], self.config["user"]["hostname"], self.config["user"]["realname"])
def nickTaken(self, args, prefix, trailing):
"""Hook that responds to 433, meaning our nick is taken"""
if self.config["ident"]["ghost"]:
@ -39,26 +39,26 @@ class Services(ModuleBase):
sleep(2)
self.bot.act_NICK(self.config["user"]["nick"][0])
self.do_initservices()
def invited(self, args, prefix, trailing):
"""Hook responding to INVITE channel invitations"""
if trailing.lower() in self.config["privatechannels"]["list"]:
self.log.info("Invited to %s, joining" % trailing)
self.bot.act_JOIN(trailing)
def do_initservices(self):
"""Identify with nickserv and join startup channels"""
" id to nickserv "
if self.config["ident"]["enable"]:
self.bot.act_PRIVMSG(self.config["ident"]["to"], self.config["ident"]["command"] % {"password":self.config["user"]["password"]})
" join plain channels "
for channel in self.config["channels"]:
self.log.info("Joining %s" % channel)
self.bot.act_JOIN(channel)
" request invite for private message channels "
for channel in self.config["privatechannels"]["list"]:
self.log.info("Requesting invite to %s" % channel)
self.bot.act_PRIVMSG(self.config["privatechannels"]["to"], self.config["privatechannels"]["command"] % {"channel":channel})

325
pyircbot/modules/UnoPlay.py Normal file
View File

@ -0,0 +1,325 @@
"""
.. module:: UnoPlay
:synopsis: Plays the Uno card game against the popular Eggdrop script "Color Uno"
.. moduleauthor:: Dave Pedu <dave@davepedu.com>
"""
from pyircbot.modulebase import ModuleBase, ModuleHook
from random import randint
import time
import re
from operator import itemgetter
from threading import Thread
class UnoPlay(ModuleBase):
def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName)
self.servicesModule = self.bot.getmodulebyname("Services")
self.mynick = self.servicesModule.config["user"]["nick"][0]
self.hooks = [ModuleHook("PRIVMSG", self.unoplay),
ModuleHook("PRIVMSG", self.trigger),
ModuleHook("NOTICE", self.decklisten)]
self.current_card = None
self.shouldgo = False
self.has_drawn = False
self.has_joined = False
self.games_played = 0
self.cards = []
def trigger(self, args, prefix, trailing):
if self.config["enable_trigger"] and "!jo" in trailing:
self.bot.act_PRIVMSG(self.config["unochannel"], "jo")
elif trailing == "jo":
# Reset streak counter & join if another human joins
self.games_played = 0
self.join_game()
def join_game(self):
if not self.has_joined:
self.sleep("joingame")
self.has_joined = True
self.bot.act_PRIVMSG(self.config["unochannel"], "jo")
def unoplay(self, args, prefix, trailing):
ogtrailing = trailing
trailing = self.stripcolors(trailing)
self.log.debug("> %s" % ogtrailing)
self.log.debug("> %s" % trailing)
if self.config["unobot"] not in prefix:
return
# Parse card from beginning message
# See if we play first
if "plays first..." in trailing:
message = trailing.split("The top card is")[1]
self.log.debug(">> we play first!")
self.current_card = self.parsecard(message)
self.log.debug(">> top card: %s" % str(self.current_card))
if self.mynick in trailing:
self.shouldgo = True
# See if someone passed to us
if "passes" in trailing and self.mynick in trailing:
self.shouldgo = True
# Play after someone was droppped
if "continuing with" in trailing and self.mynick in trailing:
self.shouldgo = True
# Parse misc played cards
# bug
if " plays " in trailing:
message = trailing.split(" plays ")[1].split(" ")[0]
self.current_card = self.parsecard(message)
self.log.debug(">> current card: %s" % str(self.current_card))
# After someone plays to us
if "to %s" % self.mynick in trailing:
self.shouldgo = True
# After color change
if "play continues with " in trailing:
color = trailing.split(" chose ")[1].split(" , ")[0].strip()
self.current_card[2]['color'] = {'Blue': 'b', 'Red': 'r', 'Yellow': 'y', 'Green': 'g'}[color]
self.current_card[2]['number'] = -1
self.current_card[2]['type'] = None
self.log.debug("Color changed to %s " % self.current_card[2]['color'])
# After color change by bot
if "Current player " in trailing and "and chooses" in trailing:
color = trailing.split(" and chooses ")[1].split(" Current ")[0].strip()
self.current_card[2]['color'] = {'Blue': 'b', 'Red': 'r', 'Yellow': 'y', 'Green': 'g'}[color]
self.current_card[2]['number'] = -1
self.current_card[2]['type'] = None
self.log.debug("Color changed to %s " % self.current_card[2]['color'])
if "urrent player %s" % self.mynick in trailing:
self.shouldgo = True
# After color change to us
if "play continues with %s" % self.mynick in trailing:
self.shouldgo = True
# We need to choose a color
if "hoose a color %s" % self.mynick in trailing:
self.pickcolor()
# Reset
if " by Marky" in trailing or "cards played in" in trailing:
self.log.debug(">> Reset")
self.current_card = None
self.shouldgo = False
self.has_drawn = False
self.has_joined = False
self.cards = []
if self.config["enable_autojoin"]:
if "to join uno" in trailing:
if self.games_played >= self.config["streak_max"]:
self.games_played = 0
return
else:
self.join_game()
def send_later(self, channel, msg, area):
Thread(target=self._send_later, args=(self.bot.act_PRIVMSG, (channel, msg, ), area)).start()
def _send_later(self, method, args, kwargs, area):
self.sleep(area)
method(*args, **kwargs)
def sleep(self, area):
if self.config["enable_delays"]:
sleep_min, sleep_max = self.config["delay_{}".format(area)]
sleep_time = randint(sleep_min * 10, sleep_max * 10) / 10
self.log.debug("Sleeping {}s for {}".format(sleep_time, area))
time.sleep(sleep_time)
def pickcolor(self):
mycolors = {"r": 0, "g": 0, "b": 0, "y": 0}
for card in self.cards:
if card[2]["color"] in mycolors.keys():
mycolors[card[2]["color"]] += 1
mycolors = sorted(mycolors.items(), key=itemgetter(1))
mycolors.reverse()
self.log.debug("Sorted: %s" % str(mycolors))
self.sleep("beforepickcolor")
self.bot.act_PRIVMSG(self.config["unochannel"], "co %s" % mycolors[0][0])
def taketurn(self):
self.shouldgo = False
move = self.getbestmove()
if move is None:
if self.has_drawn:
self.has_drawn = False
self.shouldgo = False
self.bot.act_PRIVMSG(self.config["unochannel"], "pa")
else:
self.has_drawn = True
self.shouldgo = True
self.sleep("beforedraw")
self.bot.act_PRIVMSG(self.config["unochannel"], "dr")
return
self.has_drawn = False
if self.config["enable_randomhuman"]:
if randint(1, self.config["randomhuman_chance"]) == 1:
self.bot.act_PRIVMSG(self.config["unochannel"], "ct")
if self.config["enable_delays"]:
time.sleep(self.config["randomhuman_sleep"])
self.sleep("beforemove")
self.log.debug(">> playing %s" % move[0])
self.playcard(move[0])
def getbestmove(self):
# Play anything thats not a wild
for card in self.cards:
# Skip wilds for now
if card[0] in ["wd4", "w"]:
continue
if self.validate(card, self.current_card):
return card
# Play anything
for card in self.cards:
if self.validate(card, self.current_card):
return card
# Give up
return None
def validate(self, newcard, basecard):
nc = newcard[2]
bc = basecard[2]
# Wilds can always be played
if nc["type"] in ["wd4", "w"]:
return True
# Color matches can always be played
if nc["color"] == bc["color"]:
return True
# Matching numbers are ok
if nc["type"] == "num" and bc["type"] == "num" and nc["number"] == bc["number"]:
return True
# type matches can always be played unless its a number
if nc["type"] == bc["type"] and not bc["type"] == "num":
return True
self.log.debug("invalid: %s on %s)" % (nc, bc))
return False
def playcard(self, card):
self.bot.act_PRIVMSG(self.config["unochannel"], "pl %s" % card)
def decklisten(self, args, prefix, trailing):
if self.config["unobot"] not in prefix:
return
ogtrailing = trailing
trailing = self.stripcolors(trailing)
self.log.debug("> %s" % (ogtrailing, ))
self.log.debug("> %s" % (trailing, ))
cards = []
for carddata in trailing.split(" "):
carddata = carddata.strip()
cards.append(self.parsecard(carddata))
cards.sort(key=lambda tup: tup[1])
cards.reverse()
self.cards = cards
self.log.debug(cards)
if self.shouldgo:
self.shouldgo = False
self.taketurn()
def parsecard(self, input):
# returns a card, weight tuple
self.log.debug("Parse %s" % input)
# Colors
colors = {
'r': 'Red',
'y': 'Yellow',
'b': 'Blue',
'g': 'Green'
}
# cards that don't have a color
uncolored_cards = ['wd4', 'w']
# types of cards
card_types = [
('wd4', 'Draw Four'),
('w', 'WI LD'),
('d2', 'Two'),
('r', 'Reverse'),
('s', 'Skip')
]
weights = {
'wd4': 30,
'w': 20,
'd2': 15,
'r': 14,
's': 14
}
card = ""
weight = 0
cardinfo = {"type": None, "color": None, "number": None}
for duo in card_types:
key = duo[0]
value = duo[1]
if value in input:
if key in uncolored_cards:
cardinfo["type"] = key
return (key, weights[key], cardinfo)
else:
card = key
if key in weights:
weight = weights[key]
break
cardinfo["type"] = card
if card == "":
# If we're here, the card has to be a number
# ghetto parse it
cardnumstr = ""
for i in input:
try:
cardnumstr += str(int(i))
except:
pass
card = cardnumstr
weight = int(cardnumstr)
cardinfo["type"] = "num"
cardinfo["number"] = weight
for color in colors:
if colors[color] in input:
cardinfo["color"] = color
return (color + card, weight, cardinfo)
def stripcolors(self, input):
return re.sub(r'\\x0([23])(([0-9]{1,2}((,[0-9]{1,2})?))?)', '', repr(input))[1:-1]
# def ondisable(self):
# pass

View File

@ -372,10 +372,20 @@ class PyIRCBot:
return False
# Verified! Return the set.
<<<<<<< HEAD
return ParsedCommand(command,
args.split(" "),
args,
message)
=======
ob = type('ParsedCommand', (object,), {})
ob.command = command
ob.args = [] if args=="" else args.split(" ")
ob.args_str = args
ob.message = message
return ob
# return (True, command, args, message)
>>>>>>> Add unoplayer module
@staticmethod
def load(filepath):