sopel-mafia

an IRC game module for sopel, based on mafia with a cyber theme
git clone https://0x1A4.1337.cx/code2/sopel-mafia.git
Log | Files | Refs | LICENSE

commit 459019b20aa19808cae20600b5230732afc45f9b
parent 016983bce7acbcd9fe16c7b9e7c390967cd6a61f
Author: tx <trqx.goat.si>
Date:   Wed Jan 18 16:06:29 +0100

Merge branch 'dev'

Diffstat:
README.md | 6++----
mafia.py | 249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 214 insertions(+), 41 deletions(-)
diff --git a/README.md b/README.md @@ -2,10 +2,8 @@ WIP: a mafia game module for sopel irc bot -Game should be playable up to 15 players, but roles in todo do not have any special actions yet +Game should be playable up to 15 players TODO: -* sheriff role -* agent role -* detective role +* intense testing diff --git a/mafia.py b/mafia.py @@ -13,18 +13,20 @@ class Game(object): self.reinit() def reinit(self): + self.checked = None + self.altered = False + self.files = {} self.status = 'stopped' self.players = [] self.roles = {} self.starter = None self.deads = [] self.reveal = True - self.day = 0 - self.phase = 'day' + self.day = 1 + self.phase = 'night' self.killvotes = {} self.target = None self.saved = None - self.saved_by = None # start the game def start(self, bot, nick): @@ -60,9 +62,9 @@ class Game(object): self.mute_chan() self.voice_players() self.bot.say('game has started!', self.chan.name) - self.send_status(self.chan.name) self.distribute_roles() - self.next_phase() + self.announce_phase() + self.send_status(self.chan.name) # mute channel depending of the game stat def mute_chan(self): @@ -160,8 +162,9 @@ class Game(object): p = random.choice(pool) pool.remove(p) self.roles[p] = 'killer' - - + + # create the sheriff files + self.files = dict(self.roles) # send roles for player in self.players: @@ -176,15 +179,30 @@ class Game(object): if role == 'doc': self.bot.say(f'as a {role} you can attempt to save a player from the Mafia', player) self.bot.say(f'send "save <nick>" during daytime and before end of vote to do so', player) - elif role== 'killer': + elif role == 'killer': self.bot.say(f'as a {role} you can vote to kill another player', player) self.bot.say(f'send "kill <nick>" during nighttime to do so', player) + elif role == 'agent': + self.bot.say(f'as an {role} you can alter file of a citizen or scum', player) + self.bot.say(f'send "alter <nick>" during nighttime to do so', player) + elif role == 'sheriff': + self.bot.say(f'as a {role} you can check the file of any player during the night, results will appear in the morning', player) + self.bot.say(f'send "check <nick>" during nighttime to do so', player) + elif role == 'detective': + self.bot.say('as long as you are alive, victimes can be identified', player) # send list of roles self.bot.say('roles have been distributed:', self.chan.name) nroles = self.get_roles_count() for k, v in nroles.items(): self.bot.say(f'{k}: {v}', self.chan.name) + + # inform the mafia who they are + scums = self.get_scums() + for scum in scums: + for other in scums: + if scum != other: + self.bot.say(f'{other} is part of the mafia ({self.roles[other]})', scum) # return a dict with count of player per roles def get_roles_count(self): @@ -197,32 +215,70 @@ class Game(object): return nroles + # return true if all actions are done + def actions_done(self): + if not self.vote_pass(): + print('wait vote pass') + return False + + # sheriff + if self.phase == 'night': + sheriff = self.get_special('sheriff') + print(sheriff, self.checked) + if sheriff is not None: + print('check sheriff action') + if self.checked is None: + print('wait sheriff') + return False + + # doc + if self.phase == 'day': + doc = self.get_special('doc') + if doc is not None: + if self.saved is None: + print('wait doc') + return False + # agent + if self.phase == 'night': + agent = self.get_special('agent') + if agent is not None: + if not self.altered: + print('wait agent') + return False + print('actions done') + return True + # advance day / night cycle def next_phase(self): - if self.day == 0: - self.day +=1 - + if not self.actions_done(): + return self.killvotes = {} # check/announce kills - if self.target != None: - if self.target == self.saved and self.phase == 'night' and self.saved_by not in self.deads: - self.bot.say(f'the Mafia attempted to kill {self.target} but he was saved by the doctor', self.chan.name) + if self.target is not None: + if self.target == self.saved and self.phase == 'night' and self.get_special('doc') is not None: + self.bot.say(f'Newspapers announce that the Mafia attempted to kill {self.target} but he was saved by the doctor', self.chan.name) else: - self.bot.say(f'{self.target} has been killed!', self.chan.name) + self.bot.say(f'Newspaper announces that {self.target} has been killed!', self.chan.name) self.bot.write((f'MODE {self.chan.name} -v {self.target}',)) - if self.reveal: - self.bot.say(f'{self.target} was a {self.roles[self.target]}!', self.chan.name) + if self.reveal or (self.roles[self.target] != 'detective' and self.get_special('detective') is not None): + self.bot.say(f'"looks like {self.target} was a {self.roles[self.target]}" says the article', self.chan.name) self.deads.append(self.target) self.roles[self.target] = 'dead' self.target = None # reset saved - if self.saved == 'day': - self.saved = None - self.saved_by = None + self.saved = None + + + # send checked player to detective + if self.checked is not None: + sheriff = self.get_special('sheriff') + self.bot.say(f'after examining the file of {self.checked}, you find out it is a {self.files[self.checked]}', sheriff) + self.checked = None + self.altered = False # check for game over if len(self.get_scums()) == 0: @@ -234,17 +290,24 @@ class Game(object): self.end() return - # night + # change phase + day if self.phase == 'night': self.phase = 'day' self.day += 1 - weather = random.choice(('foggy', 'sunny', 'cold', 'rainy', 'muddy', 'snowy', 'hot')) - self.bot.say(f'citizens awake in a {weather} morning and must do their actions', self.chan.name) - # day else: self.phase = 'night' - self.bot.say(f'the night falls on {self.chan.name}, scums choose their actions') + self.announce_phase() + # announce phase + def announce_phase(self): + # night + if self.phase == 'night': + self.bot.say(f'the night falls on {self.chan.name}, scums choose their actions') + # day + else: + weather = random.choice(('foggy', 'sunny', 'cold', 'rainy', 'muddy', 'snowy', 'hot')) + self.bot.say(f'citizens awake in a {weather} morning and must do their actions', self.chan.name) + # end game def end(self): self.reinit() @@ -258,6 +321,13 @@ class Game(object): alives.append(p) return alives + # return the player with a special role + def get_special(self, role): + for n, r in self.roles.items(): + if r == role: + return n + return None + # send status to chan def send_status(self, target): status = self.get_status() @@ -320,6 +390,8 @@ class Game(object): if self.status != 'running': self.bot.say(f'game is {self.status}', nick) return + if not nick in self.players: + self.bot.say('you don\'t play this game!', nick) if nick in self.deads: self.bot.say('You are dead!', nick) return @@ -351,28 +423,121 @@ class Game(object): # check if vote pass self.send_votes() - if self.vote_pass(target): - self.target = target - self.next_phase() + self.next_phase() + + # nick asks to alter a target + def alter(self, nick, target): + # alter for errors + if self.status != 'running': + self.bot.say(f'game is {self.status}', nick) + return + if not nick in self.players: + self.bot.say('you don\'t play this game!', nick) + if nick in self.deads: + self.bot.say('You are dead!', nick) + return + if self.phase != 'night': + self.bot.say('do this during the night!', nick) + return + if self.roles[nick] != 'agent': + self.bot.say('You are not an agent!', nick) + return + if not target in self.players: + self.bot.say(f'{target} does not exists!', nick) + return + if target == nick: + self.bot.say('you cannot alter your own file', nick) + return + if self.altered: + self.bot.say('you already altered a file tonight', nick) + return + + # all ok, alter the file + if self.roles[target] in CITIZENS: + self.files[target] = 'killer' + else: + self.files[target] = 'citizen' + self.altered = True + self.bot.say(f'{target} is now filed as a {self.files[target]}.', nick) + self.next_phase() + + # nick asks to check a target + def check(self, nick, target): + # check for errors + if self.status != 'running': + self.bot.say(f'game is {self.status}', nick) + return + if not nick in self.players: + self.bot.say('you don\'t play this game!', nick) + if nick in self.deads: + self.bot.say('You are dead!', nick) + return + if self.roles[nick] != 'sheriff': + self.bot.say('You are not a sheriff!', nick) + return + if self.phase != 'night': + self.bot.say('do this during the night!', nick) + return + if not target in self.players: + self.bot.say(f'{target} does not exists!', nick) + return + if target == nick: + self.bot.say(f'are you an idiot? you know you are a {self.roles[nick]}', nick) + return + + # all ok, check the file + self.checked = target + self.bot.say(f"You'll get results on {target}'s file next morning", nick) + self.next_phase() def send_votes(self): for killer in self.get_killers(): - self.bot.say('current votes:', killer) + n = self.get_minimum_votes() + self.bot.say(f'day {self.day} / {self.phase} votes, {n} votes needed:', killer) for target in self.killvotes: self.bot.say('{target}: {votes} ({n})'.format( target = target, votes = ', '.join(self.killvotes[target]), n = len(self.killvotes[target])), killer) - def vote_pass(self, target): + # return true if vote passed + def vote_pass(self): + killers = self.get_killers() + print(killers) + print(self.killvotes) + + # check if all have voted + n = 0 + for target, voters in self.killvotes.items(): + n += len(self.killvotes[target]) + if len(killers) > n: + print('not all killers have voted') + return False + + minimum = self.get_minimum_votes() + # check if vote attained the minimum + for target, voters in self.killvotes.items(): + if len(self.killvotes[target]) >= minimum: + self.target = target + print('vote passed') + return True + print('vote did not pass') + return False + + # return minimum of votes needed to kill a target + def get_minimum_votes(self): killers = self.get_killers() + # unanimous voting by night if self.phase == 'night': - # unanimous - return len(self.killvotes[target]) == len(killers) - elif self.phase == 'day': - # majority + minimum = len(killers) + # majority voting by day + else: minimum = floor(len(killers) / 2)+1 - return len(self.killvotes[target]) >= minimum - + return minimum def save(self, nick, target): # check for errors + if self.status != 'running': + self.bot.say(f'game is {self.status}', nick) + return + if not nick in self.players: + self.bot.say('you don\'t play this game!', nick) if nick in self.deads: self.bot.say(f'you are dead!', nick) return @@ -394,8 +559,8 @@ class Game(object): return self.saved = target - self.saved_by = nick self.bot.say(f"Tonight you'll attempt to save {target}", nick) + self.next_phase() @@ -425,4 +590,14 @@ def cmd_save(bot, trigger): def cmd_kill(bot, trigger): game.kill(trigger.nick, trigger.group(1)) +@module.require_privmsg(message='Use private messages, you idiot!') +@module.rule("check (.*)") +def cmd_check(bot, trigger): + game.check(trigger.nick, trigger.group(1)) + +@module.require_privmsg(message='Use private messages, you idiot!') +@module.rule("alter (.*)") +def cmd_alter(bot, trigger): + game.alter(trigger.nick, trigger.group(1)) + game = Game()