diff --git a/mafia.ipynb b/mafia.ipynb deleted file mode 100644 index e2065ee..0000000 --- a/mafia.ipynb +++ /dev/null @@ -1,117 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import mafia\n", - "import collections" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "def simple_strat(gs):\n", - " # no detectives\n", - " if gs.time == 0:\n", - " choices = [x for x in mafia.day_outcomes(gs).keys()]\n", - " tr = mafia.total_remaining(gs)\n", - " action = dict([(x,gs.players[x[0]]/tr) for x in choices])\n", - " return action\n", - " if gs.time == 1:\n", - " choices = [x for x in mafia.night_outcomes(gs).keys()]\n", - " tr = mafia.citizens_remaining(gs)\n", - " action = dict([(x,gs.players[x[0]]/tr) for x in choices])\n", - " return action " - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "pl = mafia.Players(2,0,0,19,0,0,0,0,0,0,0,0)\n", - "gs = mafia.Gamestate(1,0,None,pl)\n", - "games = mafia.make_game(pl,gs)\n", - "weight_dict = eval_strat_rc(games,simple_strat)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "cleaves = []\n", - "mleaves = []\n", - "for g in games:\n", - " cleaves.extend([x.identifier for x in g.leaves() if mafia.winner(x.data[1]) == 1])\n", - " mleaves.extend([x.identifier for x in g.leaves() if mafia.winner(x.data[1]) == -1])" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "mafia_win = sum([weight_dict[x] for x in mleaves])\n", - "citizen_win = sum([weight_dict[x] for x in cleaves])" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0.49290131952670657, 0.5070986804732934)" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "mafia_win,citizen_win" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.1" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/mafia.py b/mafia.py index e9070bf..04cafcf 100644 --- a/mafia.py +++ b/mafia.py @@ -2,137 +2,184 @@ import typing import enum import collections +from fractions import Fraction + class PType(enum.IntEnum): - Mafia=0 - VerifiedMafia=1 - PeekedMafia=2 - Citizen=3 - VerifiedCitizen=4 - PeekedCitizen=5 - Detective=6 - VerifiedDetective=7 - CitizenDetective=8 - Bodyguard=9 - VerifiedBodyguard=10 - PeekedBodyguard=11 + Mafia = 0 + VerifiedMafia = 1 + PeekedMafia = 2 + Citizen = 3 + VerifiedCitizen = 4 + PeekedCitizen = 5 + Detective = 6 + VerifiedDetective = 7 + CitizenDetective = 8 + Bodyguard = 9 + VerifiedBodyguard = 10 + PeekedBodyguard = 11 + peek_dict = { - PType.Mafia:PType.PeekedMafia, - PType.Bodyguard:PType.PeekedBodyguard, - PType.Citizen:PType.PeekedCitizen, - } + PType.Mafia: PType.PeekedMafia, + PType.Bodyguard: PType.PeekedBodyguard, + PType.Citizen: PType.PeekedCitizen, +} unpeek_dict = { - PType.PeekedMafia:PType.Mafia, - PType.PeekedBodyguard:PType.Bodyguard, - PType.PeekedCitizen:PType.Citizen, + PType.PeekedMafia: PType.Mafia, + PType.PeekedBodyguard: PType.Bodyguard, + PType.PeekedCitizen: PType.Citizen, } verified_dict = { - PType.Mafia:PType.VerifiedMafia, - PType.Bodyguard:PType.VerifiedBodyguard, - PType.Citizen:PType.VerifiedCitizen, - PType.Detective:PType.VerifiedDetective, - PType.PeekedMafia:PType.VerifiedMafia, - PType.PeekedBodyguard:PType.VerifiedBodyguard, - PType.PeekedCitizen:PType.VerifiedCitizen, + PType.Mafia: PType.VerifiedMafia, + PType.Bodyguard: PType.VerifiedBodyguard, + PType.Citizen: PType.VerifiedCitizen, + PType.Detective: PType.VerifiedDetective, + PType.PeekedMafia: PType.VerifiedMafia, + PType.PeekedBodyguard: PType.VerifiedBodyguard, + PType.PeekedCitizen: PType.VerifiedCitizen, } - -def is_citizen(x:PType) -> bool: - return x.value >=3 -def is_mafia(x:PType) -> bool: - return x.value <=2 -def is_bodyguard(x:PType) -> bool: - return (x >= 9 and x<=11) +def is_citizen(x: PType) -> bool: + return x.value >= 3 + + +def is_mafia(x: PType) -> bool: + return x.value <= 2 + + +def is_bodyguard(x: PType) -> bool: + return x >= 9 and x <= 11 + + +def is_detective(x: PType) -> bool: + return x >= 6 and x <= 8 -def is_detective(x:PType) -> bool: - return (x >= 6 and x<=8) -def is_verified(x:PType) -> bool: - return x in [1,4,7,10] +def is_verified(x: PType) -> bool: + return x in [1, 4, 7, 10] -def is_peeked(x:PType) -> bool: - return x in [2,5,11] -def peekable_types() -> typing.Tuple[PType,PType,PType]: +def is_peeked(x: PType) -> bool: + return x in [2, 5, 11] + + +def is_suspicious(x: PType) -> bool: + return x in [0, 2, 3, 5, 6, 9, 11] + + +def peekable_types() -> typing.Tuple[PType, PType, PType]: return (PType.Mafia, PType.Bodyguard, PType.Citizen) -def peeked_types() -> typing.Tuple[PType,PType,PType]: + +def peeked_types() -> typing.Tuple[PType, PType, PType]: return PType.PeekedMafia, PType.PeekedBodyguard, PType.PeekedCitizen -def peeked_version(x:PType) -> PType: + +def peeked_version(x: PType) -> PType: if x in peek_dict: return peek_dict[x] else: return None -def unpeeked_version(x:PType) -> PType: + +def unpeeked_version(x: PType) -> PType: if x in unpeek_dict: return unpeek_dict[x] else: return None - -def verified_version(x:PType) -> PType: + + +def verified_version(x: PType) -> PType: if x in verified_dict: return verified_dict[x] else: return None - -def to_string(x:PType) -> str: + + +def to_string(x: PType) -> str: if x is None: - return 'XX' - abbrevs = ['MM','MV','PM','CC','CV','CP','DD','DV','DC','BB','BV','BP'] + return "XX" + abbrevs = ["MM", "MV", "PM", "CC", "CV", "CP", "DD", "DV", "DC", "BB", "BV", "BP"] return abbrevs[x] + class Players(typing.NamedTuple): - mafia:int - verified_mafia:int - peeked_mafia:int - citizen:int - verified_citizen:int - peeked_citizen:int - detective:int - verified_detective:int - citizen_detective:int - bodyguard:int - verified_bodyguard:int - peeked_bodyguard:int - - def __add__(self,other): - new_vals = tuple( [x+y for x,y in zip(list(self),list(other)) ] ) + mafia: int + verified_mafia: int + peeked_mafia: int + citizen: int + verified_citizen: int + peeked_citizen: int + detective: int + verified_detective: int + citizen_detective: int + bodyguard: int + verified_bodyguard: int + peeked_bodyguard: int + + def __add__(self, other): + new_vals = tuple([x + y for x, y in zip(list(self), list(other))]) return Players(*new_vals) - + def __repr__(self): rlist = [] - for i in range(0,len(self),3): - rlist.append("".join([str(x) for x in self[i:i+3]])) + for i in range(0, len(self), 3): + rlist.append("".join([str(x) for x in self[i : i + 3]])) return "/".join(rlist) + class Gamestate(typing.NamedTuple): - day:int - time:int #0=day,1=night - last_protected:object - players:Players - -def total_remaining(x:Gamestate) -> int: + day: int + time: int # 0=day,1=night + last_protected: object + players: Players + + +def total_remaining(x: Gamestate) -> int: return mafia_remaining(x) + citizens_remaining(x) - -def mafia_remaining(x:Gamestate) -> int: - return sum(x.players[PType.Mafia:PType.Citizen]) -def citizens_remaining(x:Gamestate) -> int: - return sum(x.players[PType.Citizen:]) -def bodyguard_alive(x:Gamestate) -> bool: - return sum(x.players[PType.Bodyguard:]) > 0 +def mafia_remaining(x: Gamestate) -> int: + return sum(x.players[PType.Mafia : PType.Citizen]) + + +def citizens_remaining(x: Gamestate) -> int: + return sum(x.players[PType.Citizen :]) + + +def suspicious_remaining(x: Gamestate) -> int: + """ + Suspicious is anyone who _could_ be mafia, in other words, + not verified safe + """ + return ( + x.players[PType.Mafia] + + x.players[PType.PeekedMafia] + + x.players[PType.Citizen] + + x.players[PType.PeekedCitizen] + + x.players[PType.Detective] + + x.players[PType.Bodyguard] + + x.players[PType.PeekedBodyguard] + ) + + +def bodyguard_alive(x: Gamestate) -> bool: + return sum(x.players[PType.Bodyguard :]) > 0 + + +def detective_alive(x: Gamestate) -> bool: + return sum(x.players[PType.Detective : PType.Bodyguard]) > 0 + -def detective_alive(x:Gamestate) -> bool: - return sum(x.players[PType.Detective:PType.Bodyguard]) > 0 +def detective_is_already_out(gs: Gamestate) -> bool: + return gs.players[PType.VerifiedDetective] > 0 -def winner(gs:Gamestate) -> int: + +def winner(gs: Gamestate) -> int: """1 = town, 0 = not over, -1 = mafia""" mr = mafia_remaining(gs) if mr == 0: @@ -141,32 +188,35 @@ def winner(gs:Gamestate) -> int: return -1 else: return 0 - -def kill_change(ptype:PType) -> Players: + + +def kill_change(ptype: PType) -> Players: """ - produces a Players 'delta' vector that can be added to another + produces a Players 'delta' vector that can be added to another Players vector to kill one player of the provided type """ - ch = [0]*len(PType) + ch = [0] * len(PType) ch[ptype] = -1 return Players(*tuple(ch)) -def peek_change(ptype:PType) -> Players: + +def peek_change(ptype: PType) -> Players: """ - produces a Players 'delta' vector that can be added to another + produces a Players 'delta' vector that can be added to another Players vector to peek one player of the provided type """ - ch = [0]*len(PType) + ch = [0] * len(PType) ch[ptype] = -1 ch[peeked_version(ptype)] = 1 return Players(*tuple(ch)) -def verified_change(ptype:PType) -> Players: + +def verified_change(ptype: PType) -> Players: """ - produces a Players 'delta' vector that can be added to another + produces a Players 'delta' vector that can be added to another Players vector to verify one player of the provided type """ - ch = [0]*len(PType) + ch = [0] * len(PType) ch[ptype] = -1 ch[verified_version(ptype)] = 1 return Players(*tuple(ch)) @@ -174,9 +224,9 @@ def verified_change(ptype:PType) -> Players: def gs_choices(gs: Gamestate) -> list: """ - Returns a list of the allowable choices from gamestate gs. + Returns a list of the allowable choices from gamestate gs. """ - if gs.time == 0: #daytime + if gs.time == 0: # daytime choices = [] if gs.players[PType.Detective] + gs.players[PType.CitizenDetective] > 0: choices.append("Detective Out") @@ -185,7 +235,7 @@ def gs_choices(gs: Gamestate) -> list: choices.append(ptype) return choices - if gs.time == 1: #night. + if gs.time == 1: # night. if detective_alive(gs): dchoices = [] for ptype in peekable_types(): @@ -193,7 +243,12 @@ def gs_choices(gs: Gamestate) -> list: dchoices.append(ptype) else: dchoices = [None] - + + if not dchoices: + # if the detective is alive, but there are no peekable types left + # then dchoices is an empty list, and this causes a problem later + dchoices = [None] + if bodyguard_alive(gs): bchoices = [] for ptype in PType: @@ -201,10 +256,11 @@ def gs_choices(gs: Gamestate) -> list: bchoices.append(ptype) else: bchoices = [None] - - return dchoices,bchoices - -def detective_comes_out(gs:Gamestate): + + return dchoices, bchoices + + +def detective_comes_out(gs: Gamestate): """ Returns the gamestate resulting from the detective coming out in gamestate gs. """ @@ -217,20 +273,21 @@ def detective_comes_out(gs:Gamestate): gsp[ptype] = 0 gsp[PType.VerifiedDetective] = 1 gsp[PType.Detective] = 0 - + if is_peeked(gs.last_protected): last_protected = verified_version(gs.last_protected) else: last_protected = gs.last_protected - + new_players = Players(*tuple(gsp)) - new_gs = Gamestate(gs.day,gs.time,last_protected,new_players) + new_gs = Gamestate(gs.day, gs.time, last_protected, new_players) return new_gs -def day_outcomes(gs:Gamestate): + +def day_outcomes(gs: Gamestate): """ Returns a dictionary with keys: the possible choices that can be taken - (annotated with possible hidden combinations) and values: the resulting + (annotated with possible hidden combinations) and values: the resulting gamestates from those choices. This is for daytime outcomes. """ @@ -241,23 +298,24 @@ def day_outcomes(gs:Gamestate): if choice == "Detective Out": outcomes[choice] = detective_comes_out(gs) else: - change = [0]*len(PType) + change = [0] * len(PType) change[choice] = -1 new_players = gs.players + Players(*tuple(change)) - new_gs = Gamestate(gs.day,1,gs.last_protected,new_players) - outcomes[(choice,None,None)] = new_gs + new_gs = Gamestate(gs.day, 1, gs.last_protected, new_players) + outcomes[(choice, None, None)] = new_gs return outcomes -def night_outcomes(gs:Gamestate): + +def night_outcomes(gs: Gamestate): """ Returns a dictionary with keys: the possible choices that can be taken - (annotated with possible hidden combinations) and values: the resulting + (annotated with possible hidden combinations) and values: the resulting gamestates from those choices. - This is for nighttime outcomes. Only detective and bodyguard implemented + This is for nighttime outcomes. Only detective and bodyguard implemented here now. """ assert gs.time == 1 - #mafia can kill any citizen. + # mafia can kill any citizen. mafia_kills = [] for ptype in PType: if is_citizen(ptype) and gs.players[ptype] > 0: @@ -269,171 +327,271 @@ def night_outcomes(gs:Gamestate): for bg_protect in bg_protects: if detective_peek is None and bg_protect is None: new_players = gs.players + kill_change(mafia_kill) - outcomes[(mafia_kill,detective_peek,bg_protect)] = Gamestate(gs.day+1,0,None,new_players) + outcomes[(mafia_kill, detective_peek, bg_protect)] = Gamestate( + gs.day + 1, 0, None, new_players + ) elif bg_protect is None: - #then nobody is protected, figure out peek. + # then nobody is protected, figure out peek. if detective_peek != mafia_kill: - #then everything is easy... + # then everything is easy... new_players = gs.players + kill_change(mafia_kill) new_players = new_players + peek_change(detective_peek) - outcomes[(mafia_kill,detective_peek,bg_protect)] = Gamestate(gs.day+1,0,None,new_players) + outcomes[(mafia_kill, detective_peek, bg_protect)] = Gamestate( + gs.day + 1, 0, None, new_players + ) else: - #we can always have the case where the peeked gets killed + # we can always have the case where the peeked gets killed new_players = gs.players + kill_change(mafia_kill) - outcomes[(mafia_kill,detective_peek,bg_protect,0)] = Gamestate(gs.day+1,0,None,new_players) - #if there are more than 1, they could miss each other: + outcomes[ + (mafia_kill, detective_peek, bg_protect, 0) + ] = Gamestate(gs.day + 1, 0, None, new_players) + # if there are more than 1, they could miss each other: if gs.players[mafia_kill] >= 2: new_players = gs.players + kill_change(mafia_kill) new_players = new_players + peek_change(detective_peek) - outcomes[(mafia_kill,detective_peek,bg_protect,1)] = Gamestate(gs.day+1,0,None,new_players) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 1) + ] = Gamestate(gs.day + 1, 0, None, new_players) elif detective_peek is None: - #then there's no peek, just figure out BG. + # then there's no peek, just figure out BG. if bg_protect != mafia_kill: new_players = gs.players + kill_change(mafia_kill) - outcomes[(mafia_kill,detective_peek,bg_protect)] = Gamestate(gs.day+1,0,bg_protect,new_players) + outcomes[(mafia_kill, detective_peek, bg_protect)] = Gamestate( + gs.day + 1, 0, bg_protect, new_players + ) else: - #bg could save: + # bg could save: new_players = gs.players + verified_change(mafia_kill) - outcomes[(mafia_kill,detective_peek,bg_protect,2)] = Gamestate(gs.day+1,0,verified_version(bg_protect), - new_players) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 2) + ] = Gamestate( + gs.day + 1, 0, verified_version(bg_protect), new_players + ) if gs.players[mafia_kill] >= 2: new_players = gs.players + kill_change(mafia_kill) - outcomes[(mafia_kill,detective_peek,bg_protect,2)] = Gamestate(gs.day+1,0,bg_protect,new_players) - else: #a kill,a peek,a protect. - if len(set[mafia_kill,detective_peek,bg_protect]) == 3: #all different + outcomes[ + (mafia_kill, detective_peek, bg_protect, 2) + ] = Gamestate(gs.day + 1, 0, bg_protect, new_players) + else: # a kill,a peek,a protect. + if ( + len(set[mafia_kill, detective_peek, bg_protect]) == 3 + ): # all different new_players = gs.players + kill_change(mafia_kill) new_players = new_players + peek_change(detective_peek) - outcomes[(mafia_kill,detective_peek,bg_protect)] = Gamestate(gs.day+1,0,bg_protect,new_players) - if len(set[mafia_kill,detective_peek,bg_protect]) == 2: #one different, two the same + outcomes[(mafia_kill, detective_peek, bg_protect)] = Gamestate( + gs.day + 1, 0, bg_protect, new_players + ) + if ( + len(set[mafia_kill, detective_peek, bg_protect]) == 2 + ): # one different, two the same if mafia_kill == detective_peek: new_players = gs.players + kill_change(mafia_kill) - outcomes[(mafia_kill,detective_peek,bg_protect,0)] = Gamestate(gs.day+1,0,bg_protect,new_players) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 0) + ] = Gamestate(gs.day + 1, 0, bg_protect, new_players) if gs.players[mafia_kill] >= 2: new_players = gs.players + kill_change(mafia_kill) new_players = new_players + peek_change(detective_peek) - outcomes[(mafia_kill,detective_peek,bg_protect,1)] = Gamestate(gs.day+1,0,bg_protect,new_players) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 1) + ] = Gamestate(gs.day + 1, 0, bg_protect, new_players) if mafia_kill == bg_protect: new_players = gs.players + verified_change(mafia_kill) new_players = gs.players + peek_change(detective_peek) - outcomes[(mafia_kill,detective_peek,bg_protect,0)] = Gamestate(gs.day+1,0,verified_version(bg_protect),new_players) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 0) + ] = Gamestate( + gs.day + 1, 0, verified_version(bg_protect), new_players + ) if gs.players[mafia_kill] >= 2: new_players = gs.players + kill_change(mafia_kill) new_players = gs.players + peek_change(detective_peek) - outcomes[(mafia_kill,detective_peek,bg_protect,1)] = Gamestate(gs.day+1,0,bg_protect,new_players) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 1) + ] = Gamestate(gs.day + 1, 0, bg_protect, new_players) if bg_protect == detective_peek: new_players = gs.players + kill_change(mafia_kill) new_players = gs.players + peek_change(detective_peek) - outcomes[(mafia_kill,detective_peek,bg_protect,0)] = Gamestate(gs.day+1,0,bg_protect,new_players) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 0) + ] = Gamestate(gs.day + 1, 0, bg_protect, new_players) if gs.players[bg_protect] >= 2: new_players = gs.players + kill_change(mafia_kill) new_players = gs.players + peek_change(detective_peek) - outcomes[(mafia_kill,detective_peek,bg_protect,1)] = Gamestate(gs.day+1,0,peeked_version(bg_protect),new_players) - if len(set[mafia_kill,detective_peek,bg_protect]) == 1: #all the same + outcomes[ + (mafia_kill, detective_peek, bg_protect, 1) + ] = Gamestate( + gs.day + 1, + 0, + peeked_version(bg_protect), + new_players, + ) + if ( + len(set[mafia_kill, detective_peek, bg_protect]) == 1 + ): # all the same n = gs.players[mafia_kill] if n >= 1: - #0: 000 (bg saves, det peek does nothing) + # 0: 000 (bg saves, det peek does nothing) new_players = gs.players + verified_change(mafia_kill) - outcomes[(mafia_kill,detective_peek,bg_protect,0)] = Gamestate(gs.day+1,0,verified_version(bg_protect),new_players) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 0) + ] = Gamestate( + gs.day + 1, 0, verified_version(bg_protect), new_players + ) if n >= 2: - #1: 001 (so peek does nothing and bg whiffs) + # 1: 001 (so peek does nothing and bg whiffs) new_players = gs.players + verified_change(mafia_kill) - outcomes[(mafia_kill,detective_peek,bg_protect,1)] = Gamestate(gs.day+1,0,bg_protect,new_players) - #2: 010 (bg saves and detective peeks another) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 1) + ] = Gamestate(gs.day + 1, 0, bg_protect, new_players) + # 2: 010 (bg saves and detective peeks another) new_players = gs.players + verified_change(mafia_kill) new_players = gs.players + peek_change(detective_peek) - outcomes[(mafia_kill,detective_peek,bg_protect,2)] = Gamestate(gs.day+1,0,verified_version(bg_protect),new_players) - #3: 011 (so bg protects peeked player and mafia kills.) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 2) + ] = Gamestate( + gs.day + 1, 0, verified_version(bg_protect), new_players + ) + # 3: 011 (so bg protects peeked player and mafia kills.) new_players = gs.players + verified_change(mafia_kill) new_players = gs.players + peek_change(mafia_kill) - outcomes[(mafia_kill,detective_peek,bg_protect,3)] = Gamestate(gs.day+1,0,peeked_version(bg_protect),new_players) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 3) + ] = Gamestate( + gs.day + 1, 0, peeked_version(bg_protect), new_players + ) if n >= 3: - #4: 012 (so all 3 different) + # 4: 012 (so all 3 different) new_players = gs.players + verified_change(mafia_kill) new_players = gs.players + peek_change(mafia_kill) - outcomes[(mafia_kill,detective_peek,bg_protect,4)] = Gamestate(gs.day+1,0,bg_protect,new_players) + outcomes[ + (mafia_kill, detective_peek, bg_protect, 4) + ] = Gamestate(gs.day + 1, 0, bg_protect, new_players) - for key,val in outcomes.items(): + for key, val in outcomes.items(): + # if the detective dies, undo all the peeks if not detective_alive(val): gsp = list(val.players) for ptype in peeked_types(): gsp[unpeeked_version(ptype)] += gsp[ptype] gsp[ptype] = 0 - outcomes[key] = Gamestate(day=val.day,time=val.time,last_protected=val.last_protected,players=Players(*tuple(gsp))) - - - return outcomes + outcomes[key] = Gamestate( + day=val.day, + time=val.time, + last_protected=val.last_protected, + players=Players(*tuple(gsp)), + ) + if detective_is_already_out(val): + gsp = list(val.players) + for ptype in peeked_types(): + gsp[verified_version(ptype)] += gsp[ptype] + gsp[ptype] = 0 + outcomes[key] = Gamestate( + day=val.day, + time=val.time, + last_protected=val.last_protected, + players=Players(*tuple(gsp)), + ) -def expand_day_nodes(tree:treelib.Tree): + return outcomes + + +def expand_day_nodes(tree: treelib.Tree): """ Expands day nodes by creating all the appropriate children nodes. """ - + for node in tree.all_nodes(): node_id = node.identifier - path,gs,expanded = tree[node_id].data[0:3] + path, gs, expanded = tree[node_id].data[0:3] if expanded or gs.time == 1: continue - + if winner(gs) != 0: - tree[node_id].data = (path,gs,True) + tree[node_id].data = (path, gs, True) continue outcomes = day_outcomes(gs) - for key,val in outcomes.items(): + for key, val in outcomes.items(): if key == "Detective Out": - tree.create_node(f"DO {str(val)}",node_id + "_" + "DO",node_id,data=(key,val,False)) + tree.create_node( + f"DO {str(val)}", + node_id + "_" + "DO", + node_id, + data=(key, val, False), + ) else: - s = "".join([to_string(x) for x in key[0:3]]) + (str(key[-1]) if len(key) > 3 else "") - tree.create_node(f"{s} {str(val)}",node_id + "_" + s,node_id,data=(key,val,False)) - - tree[node_id].data = (path,gs,True) + s = "".join([to_string(x) for x in key[0:3]]) + ( + str(key[-1]) if len(key) > 3 else "" + ) + tree.create_node( + f"{s} {str(val)}", + node_id + "_" + s, + node_id, + data=(key, val, False), + ) + + tree[node_id].data = (path, gs, True) return -def expand_night_nodes(tree:treelib.Tree): + +def expand_night_nodes(tree: treelib.Tree): """ Expands night nodes by creating all the appropriate children nodes. """ for node in tree.all_nodes(): node_id = node.identifier - - path,gs,expanded = tree[node_id].data[0:3] + + path, gs, expanded = tree[node_id].data[0:3] if expanded or gs.time == 0: continue if winner(gs) != 0: - tree[node_id].data = (path,gs,True) + tree[node_id].data = (path, gs, True) continue outcomes = night_outcomes(gs) - for key,val in outcomes.items(): - s = "".join([to_string(x) for x in key[0:3]]) + (str(key[-1]) if len(key) > 3 else "") - tree.create_node(f"{s} {str(val)}",node_id + "_" + s,node_id,data=(key,val,False,0.0)) - - tree[node_id].data = (path,gs,True) + for key, val in outcomes.items(): + s = "".join([to_string(x) for x in key[0:3]]) + ( + str(key[-1]) if len(key) > 3 else "" + ) + tree.create_node( + f"{s} {str(val)}", + node_id + "_" + s, + node_id, + data=(key, val, False, 0.0), + ) + + tree[node_id].data = (path, gs, True) return -def unexpanded_nodes(game:treelib.Tree): + +def unexpanded_nodes(game: treelib.Tree): return sum([int(not x.data[2]) for x in game.all_nodes()]) -def unexpanded_day_nodes(game:treelib.Tree): + +def unexpanded_day_nodes(game: treelib.Tree): return sum([int((not x.data[2]) and x.data[1].time == 0) for x in game.all_nodes()]) -def unexpanded_night_nodes(game:treelib.Tree): + +def unexpanded_night_nodes(game: treelib.Tree): return sum([int((not x.data[2]) and x.data[1].time == 1) for x in game.all_nodes()]) -def level_size(tree:treelib.Tree,level=1): - return len([x for x in tree.all_nodes() if tree.level(x.identifier) == level]) -def make_game(pl:Players,gs:Gamestate): +def level_size(tree: treelib.Tree, level=1): + return len([x for x in tree.all_nodes() if tree.level(x.identifier) == level]) + + +def make_game(pl: Players, gs: Gamestate): """ makes the entire recombining game tree, which is a list of trees one for each day. """ game = treelib.Tree() - game.create_node(f'Root {str(gs)}','root0',data=(None,gs,False)) + game.create_node(f"Root {str(gs)}", "root0", data=(None, gs, False)) games = [game] g = games[-1] @@ -442,45 +600,70 @@ def make_game(pl:Players,gs:Gamestate): while unexpanded_day_nodes(g) > 0: expand_day_nodes(g) while unexpanded_night_nodes(g) > 0: - expand_night_nodes(g) + expand_night_nodes(g) gss = set([x.data[1] for x in g.leaves() if not x.data[2]]) if len(gss) > 0: root_gs = g[g.root].data[1] - new_gs = Gamestate(root_gs.day+1,0,None,pl) + new_gs = Gamestate(root_gs.day + 1, 0, None, pl) new_game = treelib.Tree() - new_game.create_node(f'Day {new_gs.day}','root' + str(new_gs.day-1),data=(None,new_gs,True,1.0)) - for idx,ugss in enumerate(gss): - new_game.create_node(f'{str(ugss)}',"root" + str(new_gs.day-1) + "_n" + str(idx), - parent=new_game.root,data=(None,ugss,False,0.0)) + new_game.create_node( + f"Day {new_gs.day}", + "root" + str(new_gs.day - 1), + data=(None, new_gs, True, 1.0), + ) + for idx, ugss in enumerate(gss): + new_game.create_node( + f"{str(ugss)}", + "root" + str(new_gs.day - 1) + "_n" + str(idx), + parent=new_game.root, + data=(None, ugss, False, 0.0), + ) games.append(new_game) g = new_game return games -def apply_strat(game,node,day,fstrat,weight_dict): +def apply_strat(game, node, day, fstrat, weight_dict): nid = node.identifier if node.is_leaf(): return - elif nid == ("root" + str(node.data[1].day-1)): - weight_dict['root' + str(day)] = 1.0 + elif nid == ("root" + str(node.data[1].day - 1)): + weight_dict["root" + str(day)] = Fraction(1, 1) if day > 0: return outcomes = fstrat(node.data[1]) - #print(outcomes) - for key,val in outcomes.items(): + # print(outcomes) + for key, val in outcomes.items(): node_ids = [x.identifier for x in game.children(nid) if x.data[0] == key] weight_dict[node_ids[0]] += val * weight_dict[nid] return -def eval_strat_rc(games,fstrat): - weight_dict = collections.defaultdict(float) + +def eval_strat_rc(games, fstrat): + weight_dict = collections.defaultdict(Fraction) for idx, t in enumerate(games): for node in t.expand_tree(mode=t.WIDTH): - apply_strat(games[idx],t[node],idx,fstrat,weight_dict) + apply_strat(games[idx], t[node], idx, fstrat, weight_dict) for node in t.leaves(): if winner(node.data[1]) != 0: continue else: - target = [x.identifier for x in games[idx+1].children(games[idx+1].root) if x.data[1] == node.data[1]][0] + target = [ + x.identifier + for x in games[idx + 1].children(games[idx + 1].root) + if x.data[1] == node.data[1] + ][0] weight_dict[target] += weight_dict[node.identifier] - return weight_dict \ No newline at end of file + return weight_dict + + +def winner_probabilities(games, weight_dict): + cleaves = [] + mleaves = [] + for g in games: + cleaves.extend([x.identifier for x in g.leaves() if winner(x.data[1]) == 1]) + mleaves.extend([x.identifier for x in g.leaves() if winner(x.data[1]) == -1]) + + mafia_win = sum([weight_dict[x] for x in mleaves]) + citizen_win = sum([weight_dict[x] for x in cleaves]) + return mafia_win, citizen_win diff --git a/main.py b/main.py new file mode 100644 index 0000000..3249dbd --- /dev/null +++ b/main.py @@ -0,0 +1,121 @@ +import mafia +import collections +from fractions import Fraction + +from strategies import incomplete_detective, proper_detective + + +def get_all_incomplete_gs(games): + states = [] + for idx, t in enumerate(games): + for node in t.expand_tree(mode=t.WIDTH): + nid = t[node].identifier + node = t[node] + if node.is_leaf(): + continue + elif node.identifier == ("root" + str(node.data[1].day - 1)): + if idx > 0: + continue + states.append(node.data[1]) + return states + + +def get_all_choices(games): + choices = [] + for idx, t in enumerate(games): + for node in t.expand_tree(mode=t.WIDTH): + # this is weird because you are reassigning node because of + # how you copy and pasted the function + game, node, day = (games[idx], t[node], idx) + nid = node.identifier + if node.is_leaf(): + continue + elif nid == ("root" + str(node.data[1].day - 1)): + if day > 0: + continue + if node.data[1].time == 0: + for k, v in mafia.day_outcomes(node.data[1]).items(): + choices.append({"before": node.data[1], "choice": k, "after": v}) + elif node.data[1].time == 1: + for k, v in mafia.night_outcomes(node.data[1]).items(): + choices.append({"before": node.data[1], "choice": k, "after": v}) + else: + raise Exception("This case should not happen") + return choices + + +def query_choices(choices, choice, before_phase=(0, 1)): + if isinstance(before_phase, int): + before_phase = (before_phase,) + return [ + c for c in choices if c["choice"] == choice and c["before"].time in before_phase + ] + + +def unique_choices(choices, before_phase=(0, 1)): + if isinstance(before_phase, int): + before_phase = (before_phase,) + uniqc = {} + for c in choices: + if c["before"].time in before_phase: + if c["choice"] not in uniqc.keys(): + uniqc[c["choice"]] = [] + uniqc[c["choice"]].append(c["before"]) + return uniqc + + +def new_game(num_mafia, num_citizen, num_detective, num_bodyguard): + pl = mafia.Players( + num_mafia, 0, 0, num_citizen, 0, 0, num_detective, 0, 0, num_bodyguard, 0, 0 + ) + gs = mafia.Gamestate(1, 0, None, pl) + games = mafia.make_game(pl, gs) + return pl, gs, games + + +def frac_to_pct(frac): + rf = round(frac, 4) + return rf.numerator / rf.denominator + + +def example_no_detective(): + pl, gs, games = new_game(2, 19, 0, 0) + weight_dict = mafia.eval_strat_rc(games, incomplete_detective) + mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) + # ~0.4929 0.5071 + print("No Detective:", frac_to_pct(mafia_win), frac_to_pct(citizen_win)) + + +def example_with_incomplete_detective(): + pl, gs, games = new_game(2, 18, 1, 0) + weight_dict = mafia.eval_strat_rc(games, incomplete_detective) + mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) + # ~0.4929 0.5071 + print("Incomplete", frac_to_pct(mafia_win), frac_to_pct(citizen_win)) + + +def example_with_proper_detective(): + pl, gs, games = new_game(2, 18, 1, 0) + weight_dict = mafia.eval_strat_rc(games, proper_detective) + mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) + # ~0.3587 0.6413 (after capabilities 1&2, did and should make citizen stronger) + # ~0.343 0.657 (after capability 3&4, did and should make citizens stronger) + # ~0.3902 0.6098 (after capability 5&6, did and should make mafia stronger) + # note the above dont mean a lot because capability 7 + # was kind of important + # ~0.3595 0.6405 (after capability 7, did and should make citizens stronger) + print("Proper:", frac_to_pct(mafia_win), frac_to_pct(citizen_win)) + + +example_no_detective() +example_with_incomplete_detective() +example_with_proper_detective() + +# create a small game so the tree is easy to inspect +pl, gs, games = new_game(2, 4, 1, 0) +choices = get_all_choices(games) +igs = get_all_incomplete_gs(games) +dgs = [i for i in igs if i.time == 0] +ngs = [i for i in igs if i.time == 1] +day_decisions = [proper_detective(i) for i in dgs] +night_decisions = [proper_detective(i) for i in ngs] diff --git a/strategies.py b/strategies.py new file mode 100644 index 0000000..70708b7 --- /dev/null +++ b/strategies.py @@ -0,0 +1,237 @@ +from fractions import Fraction + +import mafia + + +def original_strat(gs): + # no detectives + if gs.time == 0: + choices = [x for x in mafia.day_outcomes(gs).keys()] + tr = mafia.total_remaining(gs) + action = dict([(x, Fraction(gs.players[x[0]], tr)) for x in choices]) + return action + if gs.time == 1: + choices = [x for x in mafia.night_outcomes(gs).keys()] + tr = mafia.citizens_remaining(gs) + action = dict([(x, Fraction(gs.players[x[0]], tr)) for x in choices]) + return action + + +def incomplete_detective(gs): + """ + This detective is incomplete in several ways... its purpose was that it should + not crash with a detective in the game, and should have + the same strategy results as original_strat with the same number of citizens+detectives + 1. All "Detective Out" branches are ignored + 2. The case where mafia kill and detective peek the same type + we ignore one case (assign it probability zero) + 3. (As a consequence above) neither Mafia or Citizen do anything different + because the detective is in the game + + """ + if gs.time == 0: + choices = [x for x in mafia.day_outcomes(gs).keys()] + tr = mafia.total_remaining(gs) + action = dict( + [ + (x, Fraction(gs.players[x[0]], tr)) + for x in choices + if x != "Detective Out" + ] + ) + return action + if gs.time == 1: + # get all the choices for this round + choices = list(mafia.night_outcomes(gs).keys()) + + # from these choices, specifically get all the different kills + kill_choices = set([c[0] for c in choices]) + # and get the total number of possible kills + total_killable = mafia.citizens_remaining(gs) + # get the probability each unique kill choice will be chosen + kill_chances = {} + for kc in kill_choices: + num_remaining = gs.players[kc] + # kill_chances[kc] = num_remaining / total_killable + kill_chances[kc] = Fraction(num_remaining, total_killable) + # this should add up to 1 + assert sum(kill_chances.values()) == 1 + + # now do the same for peeks + peek_choices = set([c[1] for c in choices if c[1] is not None]) + if peek_choices: + # total number of possible peeks + total_unpeeked = ( + gs.players[mafia.PType.Citizen] + gs.players[mafia.PType.Mafia] + ) + # get the probability of each unique peek choice + peek_chances = {} + for pc in peek_choices: + num_remaining = gs.players[pc] + peek_chances[pc] = Fraction(num_remaining) / Fraction(total_unpeeked) + # this should add up to 1 + assert sum(peek_chances.values()) == 1 + else: + # so if there are no peeks, make sure that all peek lookups are 1 + peek_chances = {None: Fraction(1, 1)} + + action = {} + for c in choices: + killed = c[0] + peeked = c[1] + if len(c) == 4: + # this means this choice has an additional index + # i.e. sometimes detective peeks same person mafia kills, etc. + # as to keep probabilities right, only take the first option + if c[3] != 0: + action[c] = 0 + continue + # the probability of this action is the probability this type will be killed + # multiplied by the probability this type will be peeked + action[c] = kill_chances[killed] * peek_chances[peeked] + assert sum(action.values()) == 1 + return action + + +def is_time_to_come_out(gs): + """ + If Detective is alive and there is a PeekedMafia + come out 100% of the time. Otherwise, don't come out + """ + has_detective = mafia.detective_alive(gs) + detective_not_already_out = ( + gs.players[mafia.PType.VerifiedDetective] == 0 + ) # May not be necessary since if the detective is out we should have no peeks (they are all verified), but just in case... + has_peeks = gs.players[mafia.PType.PeekedMafia] > 0 + is_day = gs.time == 0 + return all([has_detective, detective_not_already_out, has_peeks, is_day]) + + +def proper_detective(gs): + """ + 1. If there's a PeekedMafia, always come out + 2. If there's a VerifiedMafia, citizens will always kill + 3. If there's a VerifiedCitizen, citizens will never kill + 4. If there's a VerifiedDetective, citizens will never kill + 5. If there's a VerifiedDetective, mafia will always assasinate + 6. If there's a VerifiedCitizen, and no VerifiedDetective, mafia will always assasinate + 7. Correctly assign random chances to the case when mafia assasinate and detective peek the same type + """ + if gs.time == 0: + choices = [x for x in mafia.day_outcomes(gs).keys()] + action = {} + if is_time_to_come_out(gs): + assert "Detective Out" in choices + for c in choices: + if c == "Detective Out": + action[c] = Fraction(1, 1) + else: + action[c] = Fraction(0, 1) + else: + # if there's a verifiedmafia, kill them first! + if gs.players[mafia.PType.VerifiedMafia] > 0: + for c in choices: + if c == "Detective Out": + action[c] = Fraction() + else: + if c[0] == mafia.PType.VerifiedMafia: + action[c] = Fraction(1, 1) + else: + action[c] = Fraction(0, 1) + else: + # Get the total remaining, excluding those who are verified safe + sr = mafia.suspicious_remaining(gs) + for x in choices: + if x == "Detective Out": + # Never come out + action[x] = Fraction() + else: + if not mafia.is_suspicious(x[0]): + # Never kill someone who is verified safe + action[x] = Fraction() + else: + # For everyone else, choose randomly with an equal chance + action[x] = Fraction(gs.players[x[0]], sr) + assert sum(action.values()) == 1 + return action + if gs.time == 1: + # get all the choices for this round + choices = list(mafia.night_outcomes(gs).keys()) + + # from these choices, specifically get all the different kills + kill_choices = set([c[0] for c in choices]) + # and get the total number of possible kills + total_killable = mafia.citizens_remaining(gs) + + # go for verified detective + # if there isnt one, go for verifieddetective + # if there isnt one, then get probability for each kill choice by how common + kill_chances = {} + # if there is a verifieddetective, kill it 100% of the time + if mafia.PType.VerifiedDetective in kill_choices: + for kc in kill_choices: + if kc == mafia.PType.VerifiedDetective: + kill_chances[kc] = Fraction(1, 1) + else: + kill_chances[kc] = Fraction(0, 1) + elif mafia.PType.VerifiedCitizen in kill_choices: + for kc in kill_choices: + if kc == mafia.PType.VerifiedCitizen: + kill_chances[kc] = Fraction(1, 1) + else: + kill_chances[kc] = Fraction(0, 1) + else: + # get the probability each unique kill choice will be chosen + for kc in kill_choices: + num_remaining = gs.players[kc] + # kill_chances[kc] = num_remaining / total_killable + kill_chances[kc] = Fraction(num_remaining, total_killable) + # this should add up to 1 + assert sum(kill_chances.values()) == 1 + + # now do the same for peeks + peek_choices = set([c[1] for c in choices if c[1] is not None]) + if peek_choices: + # total number of possible peeks + total_unpeeked = ( + gs.players[mafia.PType.Citizen] + gs.players[mafia.PType.Mafia] + ) + # get the probability of each unique peek choice + peek_chances = {} + for pc in peek_choices: + num_remaining = gs.players[pc] + peek_chances[pc] = Fraction(num_remaining) / Fraction(total_unpeeked) + # this should add up to 1 + assert sum(peek_chances.values()) == 1 + else: + # so if there are no peeks, make sure that all peek lookups are 1 + peek_chances = {None: Fraction(1, 1)} + + action = {} + for c in choices: + killed = c[0] + peeked = c[1] + if len(c) == 4: + # this means this choice has an additional index + # i.e. sometimes detective peeks same person mafia kills, etc. + # as to keep probabilities right, only take the first option + # c[3] == 0 means that the peeked player was killed + # c[3] == 1 means that they hit two different ones + case_idx = c[3] + assert case_idx in (0, 1) + assert killed == peeked # i think this is always true + assert ( + gs.players[killed] == gs.players[peeked] + ) # so this is always true + chance_it_is_the_same_player = Fraction(1, gs.players[killed]) + chance_it_isnt = 1 - chance_it_is_the_same_player + case_chances = {0: chance_it_is_the_same_player, 1: chance_it_isnt} + action[c] = ( + kill_chances[killed] * peek_chances[peeked] * case_chances[case_idx] + ) + else: + # the probability of this action is the probability this type will be killed + # multiplied by the probability this type will be peeked + action[c] = kill_chances[killed] * peek_chances[peeked] + assert sum(action.values()) == 1 + return action diff --git a/test_mafia.py b/test_mafia.py new file mode 100644 index 0000000..9dd4aba --- /dev/null +++ b/test_mafia.py @@ -0,0 +1,112 @@ +from fractions import Fraction + +import pytest + +import mafia +from main import new_game, get_all_incomplete_gs +from strategies import original_strat, incomplete_detective, proper_detective + + +def original_game(): + pl, gs, games = new_game(2, 19, 0, 0) + weight_dict = mafia.eval_strat_rc(games, original_strat) + return pl, gs, games, weight_dict + + +def test_nothing(): + assert 1 == 1 + + +def test_winner_probabilities(): + pl, gs, games, weight_dict = original_game() + mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) + assert mafia_win == Fraction(478099, 969969) + assert citizen_win == Fraction(491870, 969969) + + +def test_incomplete_detective_original_strat_same(): + pl, gs, games = new_game(2, 19, 0, 0) + weight_dict_original = mafia.eval_strat_rc(games, original_strat) + weight_dict_simple = mafia.eval_strat_rc(games, incomplete_detective) + assert weight_dict_original == weight_dict_simple + + +def test_can_play_detective(): + pl, gs, games = new_game(2, 18, 1, 0) + weight_dict = mafia.eval_strat_rc(games, incomplete_detective) + mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) + assert mafia_win + citizen_win == 1 + + +def test_leaves_match_children(): + """ + All leaves that aren't game ending should have a corresponding starter node + in the next days' game tree. This isn't checked right now until a strategy + is evaluated. + """ + pl, gs, games = new_game(2, 18, 1, 0) + for idx, t in enumerate(games): + for node in t.leaves(): + if mafia.winner(node.data[1]) != 0: + continue + else: + targets = [] + for x in games[idx + 1].children(games[idx + 1].root): + if x.data[1] == node.data[1]: + targets.append(x.identifier) + assert len(targets) == 1 + + +def test_strategy_with_detective_sum_is_one(): + pl, gs, games = new_game(2, 4, 1, 0) + mgs, cgs, dgs = [ + x.data[1] + for x in games[0].children(games[0].root) + if x.data[0] != "Detective Out" + ] + mgs_outcomes = incomplete_detective(mgs) + cgs_outcomes = incomplete_detective(cgs) + dgs_outcomes = incomplete_detective(dgs) + assert sum(mgs_outcomes.values()) == 1 + assert sum(cgs_outcomes.values()) == 1 + assert sum(dgs_outcomes.values()) == 1 + + +def test_incomplete_detective_same_as_no_detective(): + _, _, games = new_game(2, 19, 0, 0) + gwd = mafia.eval_strat_rc(games, original_strat) + _, _, dgames = new_game(2, 18, 1, 0) + dwd = mafia.eval_strat_rc(dgames, incomplete_detective) + assert mafia.winner_probabilities(games, gwd) == mafia.winner_probabilities( + dgames, dwd + ) + + +def test_can_play_proper_detective(): + pl, gs, games = new_game(2, 18, 1, 0) + weight_dict = mafia.eval_strat_rc(games, proper_detective) + mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) + assert mafia_win + citizen_win == 1 + + +def test_proper_detective_beats_incomplete_detective(): + _, _, games = new_game(2, 18, 1, 0) + idwd = mafia.eval_strat_rc(games, incomplete_detective) + pdwd = mafia.eval_strat_rc(games, proper_detective) + imw, icw = mafia.winner_probabilities(games, idwd) + pmw, pcw = mafia.winner_probabilities(games, pdwd) + # proper detective wins more often + assert pcw > icw + + +def test_all_peeks_verified_after_detective_out(): + dpl, dgs, dgame = new_game(2, 5, 1, 0) + igs = get_all_incomplete_gs(dgame) + bad_states = [] + for i in igs: + if ( + i.players[mafia.PType.PeekedMafia] > 0 + or i.players[mafia.PType.PeekedCitizen] > 0 + ) and i.players[mafia.PType.VerifiedDetective] > 0: + bad_states.append(i) + assert len(bad_states) == 0