diff --git a/src/events.py b/src/events.py new file mode 100644 index 0000000..05ee753 --- /dev/null +++ b/src/events.py @@ -0,0 +1,36 @@ +# event system + +EVENT_CALLBACKS = {} + +def add_listener(event, callback, priority = 5): + if event not in EVENT_CALLBACKS: + EVENT_CALLBACKS[event] = [] + + if (priority, callback) not in EVENT_CALLBACKS[event]: + EVENT_CALLBACKS[event].append((priority, callback)) + EVENT_CALLBACKS[event].sort(key = lambda x: x[0]) + +def remove_listener(event, callback, priority = 5): + if event in EVENT_CALLBACKS and (priority, callback) in EVENT_CALLBACKS[event]: + EVENT_CALLBACKS[event].remove((priority, callback)) + + +class Event: + def __init__(self, name, data): + self.stop_processing = False + self.prevent_default = False + self.name = name + self.data = data + + def dispatch(self, *args): + if self.name not in EVENT_CALLBACKS: + return + + for item in list(EVENT_CALLBACKS[self.name]): + item[1](self, *args) + if self.stop_processing: + break + + return not self.prevent_default + +# vim: set expandtab:sw=4:ts=4: diff --git a/src/settings.py b/src/settings.py index 938d8f5..c0a6fc6 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,5 +1,6 @@ from collections import defaultdict import math +from src import events PING_WAIT = 300 # Seconds PING_MIN_WAIT = 30 # How long !start has to wait after a !ping @@ -521,28 +522,58 @@ class MadMode(object): "assassin" : ( 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 ), }) -# evilvillage is broken, disable for now -#@game_mode("evilvillage", minp = 6, maxp = 18) +@game_mode("evilvillage", minp = 6, maxp = 18, likelihood = 1) class EvilVillageMode(object): """Majority of the village is wolf aligned, safes must secretly try to kill the wolves.""" def __init__(self): self.DEFAULT_ROLE = "cultist" - self.ROLE_INDEX = ( 6 , 10 , 15 ) + self.DEFAULT_SEEN_AS_VILL = False + self.ABSTAIN_ENABLED = False + self.ROLE_INDEX = ( 6 , 8 , 10 , 15 ) self.ROLE_GUIDE = reset_roles(self.ROLE_INDEX) self.ROLE_GUIDE.update({# village roles - "oracle" : ( 1 , 1 , 0 ), - "seer" : ( 0 , 0 , 1 ), - "guardian angel" : ( 0 , 1 , 1 ), - "shaman" : ( 1 , 1 , 1 ), - "hunter" : ( 0 , 0 , 1 ), - "villager" : ( 0 , 0 , 1 ), + "oracle" : ( 0 , 1 , 1 , 0 ), + "seer" : ( 0 , 0 , 0 , 1 ), + "guardian angel" : ( 0 , 0 , 1 , 1 ), + "shaman" : ( 0 , 0 , 0 , 1 ), + "hunter" : ( 1 , 1 , 1 , 1 ), + "villager" : ( 0 , 0 , 0 , 1 ), # wolf roles - "wolf" : ( 1 , 1 , 2 ), - "minion" : ( 0 , 1 , 1 ), + "wolf" : ( 1 , 1 , 1 , 2 ), + "minion" : ( 0 , 0 , 1 , 1 ), # neutral roles - "fool" : ( 0 , 1 , 1 ), + "fool" : ( 0 , 0 , 1 , 1 ), + # templates + "cursed villager" : ( 0 , 1 , 1 , 1 ), }) + def startup(self): + events.add_listener("chk_win", self.chk_win, 1) + + def teardown(self): + events.remove_listener("chk_win", self.chk_win, 1) + + def chk_win(self, evt, var, lpl, lwolves, lrealwolves): + lsafes = len(var.list_players(["oracle", "seer", "guardian angel", "shaman", "hunter", "villager"])) + evt.stop_processing = True + + try: + if lrealwolves == 0: + evt.data["winner"] = "villagers" + evt.data["message"] = ("Game over! All the wolves are dead! The villagers " + + "round up the remaining cultists, hang them, and live " + + "happily ever after.") + elif lsafes == 0: + evt.data["winner"] = "wolves" + evt.data["message"] = ("Game over! All the villagers are dead! The cultists rejoice " + + "with their wolf buddies and start plotting to take over the " + + "next village.") + elif evt.data["winner"][0] != "@": + evt.data["winner"] = None + except TypeError: # means that evt.data["winner"] isn't a string or something else subscriptable + evt.data["winner"] = None + + @game_mode("classic", minp = 7, maxp = 21, likelihood = 4) class ClassicMode(object): """Classic game mode from before all the changes.""" diff --git a/src/wolfgame.py b/src/wolfgame.py index 6a1023e..3c48706 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -40,6 +40,10 @@ from src import logger import urllib.request import sqlite3 +# done this way so that events is accessible in !eval (useful for debugging) +from src import events +Event = events.Event + debuglog = logger("debug.log", write=False, display=False) # will be True if in debug mode errlog = logger("errors.log") plog = logger(None) #use this instead of print so that logs have timestamps @@ -80,6 +84,7 @@ var.PINGING_IFS = False var.TIMERS = {} var.ORIGINAL_SETTINGS = {} +var.CURRENT_GAMEMODE = {"name": "default"} var.LAST_SAID_TIME = {} @@ -371,6 +376,10 @@ def pm(cli, target, message): # message either privmsg or notice, depending on cli.msg(target, message) def reset_settings(): + if hasattr(var.CURRENT_GAMEMODE, "teardown") and callable(var.CURRENT_GAMEMODE.teardown): + var.CURRENT_GAMEMODE.teardown() + + var.CURRENT_GAMEMODE = {"name": "default"} for attr in list(var.ORIGINAL_SETTINGS.keys()): setattr(var, attr, var.ORIGINAL_SETTINGS[attr]) dict.clear(var.ORIGINAL_SETTINGS) @@ -412,7 +421,6 @@ def reset(): var.PINGED_ALREADY_ACCS = [] var.NO_LYNCH = [] var.FGAMED = False - var.CURRENT_GAMEMODE = "default" var.GAMEMODE_VOTES = {} #list of players who have used !game reset_settings() @@ -1516,7 +1524,7 @@ def stats(cli, nick, chan, rest): else: cli.notice(nick, msg) - if var.PHASE == "join" or not var.ROLE_REVEAL or var.GAME_MODES[var.CURRENT_GAMEMODE][4]: + if var.PHASE == "join" or not var.ROLE_REVEAL or var.GAME_MODES[var.CURRENT_GAMEMODE.name][4]: return message = [] @@ -2148,7 +2156,7 @@ def stop_game(cli, winner = "", abort = False): if won or iwon: winners.append(splr) - var.update_game_stats(var.CURRENT_GAMEMODE, len(survived) + len(var.DEAD), winner) + var.update_game_stats(var.CURRENT_GAMEMODE.name, len(survived) + len(var.DEAD), winner) # spit out the list of winners winners.sort() @@ -2226,6 +2234,8 @@ def chk_win(cli, end_game = True): except KeyError: pass + winner = None + message = "" if lpl < 1: message = "Game over! There are no players remaining." winner = "none" @@ -2265,8 +2275,15 @@ def chk_win(cli, end_game = True): elif lrealwolves == 0: chk_traitor(cli) return chk_win(cli, end_game) - else: + + event = Event("chk_win", {"winner": winner, "message": message}) + event.dispatch(var, lpl, lwolves, lrealwolves) + winner = event.data["winner"] + message = event.data["message"] + + if winner is None: return False + if end_game: players = [] if winner == "monsters": @@ -5353,7 +5370,7 @@ def transition_night(cli): debuglog(elder, "ELDER DEATH") if var.FIRST_NIGHT and chk_win(cli, end_game=False): # prevent game from ending as soon as it begins (useful for the random game mode) - start(cli, botconfig.NICK, botconfig.CHANNEL, restart=var.CURRENT_GAMEMODE) + start(cli, botconfig.NICK, botconfig.CHANNEL, restart=var.CURRENT_GAMEMODE.name) return # game ended from bitten / amnesiac turning, narcolepsy totem expiring, or other weirdness @@ -5864,13 +5881,16 @@ def cgamemode(cli, arg): md = modeargs.pop(0) try: gm = var.GAME_MODES[md][0](*modeargs) + if hasattr(gm, "startup") and callable(gm.startup): + gm.startup() for attr in dir(gm): val = getattr(gm, attr) if (hasattr(var, attr) and not callable(val) and not attr.startswith("_")): var.ORIGINAL_SETTINGS[attr] = getattr(var, attr) setattr(var, attr, val) - var.CURRENT_GAMEMODE = md + gm.name = md + var.CURRENT_GAMEMODE = gm return True except var.InvalidModeException as e: cli.msg(botconfig.CHANNEL, "Invalid mode: "+str(e)) @@ -6119,7 +6139,7 @@ def start(cli, nick, chan, forced = False, restart = ""): if not restart: cli.msg(chan, ("{0}: Welcome to Werewolf, the popular detective/social party "+ - "game (a theme of Mafia). Using the \002{1}\002 game mode.").format(", ".join(pl), var.CURRENT_GAMEMODE)) + "game (a theme of Mafia). Using the \002{1}\002 game mode.").format(", ".join(pl), var.CURRENT_GAMEMODE.name)) cli.mode(chan, "+m") var.ORIGINAL_ROLES = copy.deepcopy(var.ROLES) # Make a copy @@ -6876,15 +6896,15 @@ def listroles(cli, nick, chan, rest): rest = re.split(" +", rest.strip(), 1) #message if this game mode has been disabled - if (not len(rest[0]) or rest[0].isdigit()) and var.GAME_MODES[var.CURRENT_GAMEMODE][4]: - txt += " {0}: {1}roles was disabled for the {2} game mode.".format(nick, botconfig.CMD_CHAR, var.CURRENT_GAMEMODE) + if (not len(rest[0]) or rest[0].isdigit()) and var.GAME_MODES[var.CURRENT_GAMEMODE.name][4]: + txt += " {0}: {1}roles was disabled for the {2} game mode.".format(nick, botconfig.CMD_CHAR, var.CURRENT_GAMEMODE.name) rest = [] roleindex = {} #prepend player count if called without any arguments elif not len(rest[0]) and pl > 0: txt += " {0}: There {1} \u0002{2}\u0002 playing.".format(nick, "is" if pl == 1 else "are", pl) if var.PHASE in ["night", "day"]: - txt += " Using the {0} game mode.".format(var.CURRENT_GAMEMODE) + txt += " Using the {0} game mode.".format(var.CURRENT_GAMEMODE.name) #read game mode to get roles for elif len(rest[0]) and not rest[0].isdigit(): @@ -7091,7 +7111,7 @@ def game_stats(cli, nick, chan, rest): cli.notice(nick, "Wait until the game is over to view stats.") return - gamemode = var.CURRENT_GAMEMODE + gamemode = var.CURRENT_GAMEMODE.name gamesize = None rest = rest.split() # Check for gamemode