From 4cb540d36fa4ee1b390d76f1e360778beceb780b Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sat, 17 Apr 2021 22:23:57 -0700 Subject: [PATCH 01/26] Added a test file --- test_mafia.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 test_mafia.py diff --git a/test_mafia.py b/test_mafia.py new file mode 100644 index 0000000..a1efcc9 --- /dev/null +++ b/test_mafia.py @@ -0,0 +1,2 @@ +def test_nothing(): + assert 1 == 1 From c9af42c5dc8244e390e92436d62411597f40bbee Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sat, 17 Apr 2021 22:26:32 -0700 Subject: [PATCH 02/26] Moved code from notebook into main.py --- main.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..1eb49e4 --- /dev/null +++ b/main.py @@ -0,0 +1,33 @@ +import mafia +import collections + + +def simple_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, 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, gs.players[x[0]] / tr) for x in choices]) + return action + + +pl = mafia.Players(2, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0) +gs = mafia.Gamestate(1, 0, None, pl) +games = mafia.make_game(pl, gs) +weight_dict = mafia.eval_strat_rc(games, simple_strat) + +cleaves = [] +mleaves = [] +for g in games: + cleaves.extend([x.identifier for x in g.leaves() if mafia.winner(x.data[1]) == 1]) + mleaves.extend([x.identifier for x in g.leaves() if mafia.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]) + +print(mafia_win, citizen_win) From dcfb5b947bb9b72614242237062793757c349f51 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sat, 17 Apr 2021 22:36:22 -0700 Subject: [PATCH 03/26] Deleted the python notebook --- mafia.ipynb | 117 ---------------------------------------------------- 1 file changed, 117 deletions(-) delete mode 100644 mafia.ipynb 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 -} From 233022d95b98810fb27e330af1bf8456846a832f Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sat, 17 Apr 2021 22:37:28 -0700 Subject: [PATCH 04/26] Added a test for the original game example in the notebook --- test_mafia.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test_mafia.py b/test_mafia.py index a1efcc9..9e7553a 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -1,2 +1,32 @@ +import mafia +from main import simple_strat + + +def original_game(): + pl = mafia.Players(2, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0) + gs = mafia.Gamestate(1, 0, None, pl) + games = mafia.make_game(pl, gs) + weight_dict = mafia.eval_strat_rc(games, simple_strat) + return pl, gs, games, weight_dict + + def test_nothing(): assert 1 == 1 + + +def test_original_game(): + pl, gs, games, weight_dict = original_game() + cleaves = [] + mleaves = [] + for g in games: + cleaves.extend( + [x.identifier for x in g.leaves() if mafia.winner(x.data[1]) == 1] + ) + mleaves.extend( + [x.identifier for x in g.leaves() if mafia.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]) + assert mafia_win == 0.49290131952670657 + assert citizen_win == 0.5070986804732934 From f8a2599c4e8f8b7d635792d9231493e372374839 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sat, 17 Apr 2021 22:40:01 -0700 Subject: [PATCH 05/26] Ran mafia.py through sublack --- mafia.py | 519 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 324 insertions(+), 195 deletions(-) diff --git a/mafia.py b/mafia.py index e9070bf..cd7f5fa 100644 --- a/mafia.py +++ b/mafia.py @@ -3,136 +3,158 @@ import enum import collections + 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_verified(x:PType) -> bool: - return x in [1,4,7,10] +def is_detective(x: PType) -> bool: + return x >= 6 and x <= 8 -def is_peeked(x:PType) -> bool: - return x in [2,5,11] -def peekable_types() -> typing.Tuple[PType,PType,PType]: +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]: 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 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 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 +163,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 +199,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 +210,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 +218,7 @@ def gs_choices(gs: Gamestate) -> list: dchoices.append(ptype) else: dchoices = [None] - + if bodyguard_alive(gs): bchoices = [] for ptype in PType: @@ -201,10 +226,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 +243,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 +268,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 +297,259 @@ 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 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)), + ) + + return outcomes + -def expand_day_nodes(tree:treelib.Tree): +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 +558,58 @@ 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)] = 1.0 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): + +def eval_strat_rc(games, fstrat): weight_dict = collections.defaultdict(float) 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 From 9b5f6b90e5b9379da199c85d295bfd2c70f18105 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sat, 17 Apr 2021 22:44:00 -0700 Subject: [PATCH 06/26] Moved the calculation of winning team probabilities to a function --- mafia.py | 12 ++++++++++++ main.py | 12 ++---------- test_mafia.py | 15 ++------------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/mafia.py b/mafia.py index cd7f5fa..bcd9077 100644 --- a/mafia.py +++ b/mafia.py @@ -613,3 +613,15 @@ def eval_strat_rc(games, fstrat): ][0] weight_dict[target] += weight_dict[node.identifier] 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 index 1eb49e4..716f612 100644 --- a/main.py +++ b/main.py @@ -19,15 +19,7 @@ def simple_strat(gs): pl = mafia.Players(2, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0) gs = mafia.Gamestate(1, 0, None, pl) games = mafia.make_game(pl, gs) -weight_dict = mafia.eval_strat_rc(games, simple_strat) - -cleaves = [] -mleaves = [] -for g in games: - cleaves.extend([x.identifier for x in g.leaves() if mafia.winner(x.data[1]) == 1]) - mleaves.extend([x.identifier for x in g.leaves() if mafia.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]) +weight_dict = mafia.eval_strat_rc(games, simple_strat) +mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) print(mafia_win, citizen_win) diff --git a/test_mafia.py b/test_mafia.py index 9e7553a..95f2a56 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -14,19 +14,8 @@ def test_nothing(): assert 1 == 1 -def test_original_game(): +def test_winner_probabilities(): pl, gs, games, weight_dict = original_game() - cleaves = [] - mleaves = [] - for g in games: - cleaves.extend( - [x.identifier for x in g.leaves() if mafia.winner(x.data[1]) == 1] - ) - mleaves.extend( - [x.identifier for x in g.leaves() if mafia.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]) + mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) assert mafia_win == 0.49290131952670657 assert citizen_win == 0.5070986804732934 From 2064c6d651398722e78087637b9cadec8327b8ed Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sat, 17 Apr 2021 23:02:17 -0700 Subject: [PATCH 07/26] Created a function for a new standard game --- main.py | 12 +++++++++--- test_mafia.py | 6 ++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index 716f612..b914ac4 100644 --- a/main.py +++ b/main.py @@ -16,10 +16,16 @@ def simple_strat(gs): return action -pl = mafia.Players(2, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0) -gs = mafia.Gamestate(1, 0, None, pl) -games = mafia.make_game(pl, gs) +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 + +pl, gs, games = new_game(2, 19, 0, 0) weight_dict = mafia.eval_strat_rc(games, simple_strat) mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) print(mafia_win, citizen_win) diff --git a/test_mafia.py b/test_mafia.py index 95f2a56..8697ad9 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -1,11 +1,9 @@ import mafia -from main import simple_strat +from main import new_game, simple_strat def original_game(): - pl = mafia.Players(2, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0) - gs = mafia.Gamestate(1, 0, None, pl) - games = mafia.make_game(pl, gs) + pl, gs, games = new_game(2, 19, 0, 0) weight_dict = mafia.eval_strat_rc(games, simple_strat) return pl, gs, games, weight_dict From 678e0d5e738665d55e95fbd2fdd81afda93905ae Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sat, 17 Apr 2021 23:07:41 -0700 Subject: [PATCH 08/26] Copied the original strategy and made a failing test for playing detective --- test_mafia.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test_mafia.py b/test_mafia.py index 8697ad9..be689b8 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -2,9 +2,23 @@ from main import new_game, simple_strat +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, 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, gs.players[x[0]] / tr) for x in choices]) + return action + + def original_game(): pl, gs, games = new_game(2, 19, 0, 0) - weight_dict = mafia.eval_strat_rc(games, simple_strat) + weight_dict = mafia.eval_strat_rc(games, original_strat) return pl, gs, games, weight_dict @@ -17,3 +31,10 @@ def test_winner_probabilities(): mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) assert mafia_win == 0.49290131952670657 assert citizen_win == 0.5070986804732934 + + +def test_can_play_detective(): + pl, gs, games = new_game(2, 18, 1, 0) + weight_dict = mafia.eval_strat_rc(games, simple_strat) + mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) + assert mafia_win + citizen_win == 1 From da3e3c0ab1ae0a8f98fc55b7bbe40fad9709847c Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 00:24:45 -0700 Subject: [PATCH 09/26] Added a test to compare original strategy to copy --- test_mafia.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test_mafia.py b/test_mafia.py index be689b8..db1a934 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -33,6 +33,13 @@ def test_winner_probabilities(): assert citizen_win == 0.5070986804732934 +def test_simple_strat_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, simple_strat) + 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, simple_strat) From 345ff53da3854bdf730051153ef5e08d7a5ec769 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 00:27:10 -0700 Subject: [PATCH 10/26] Added test for problematic leaves when citizens are large and there's a bodyguard, then fixed it --- mafia.py | 5 +++++ test_mafia.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/mafia.py b/mafia.py index bcd9077..485a53f 100644 --- a/mafia.py +++ b/mafia.py @@ -219,6 +219,11 @@ def gs_choices(gs: Gamestate) -> list: 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: diff --git a/test_mafia.py b/test_mafia.py index db1a934..251b10c 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -45,3 +45,24 @@ def test_can_play_detective(): weight_dict = mafia.eval_strat_rc(games, simple_strat) 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 + + From 9099ac70e630e4a7ca4399efe385c79a85638ad8 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 00:28:21 -0700 Subject: [PATCH 11/26] Added filter to keep simple_strat from crashing, then a test to make sure the probabilities of actions add up to 1 --- main.py | 4 +++- test_mafia.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index b914ac4..f70c5a9 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,9 @@ def simple_strat(gs): if gs.time == 0: choices = [x for x in mafia.day_outcomes(gs).keys()] tr = mafia.total_remaining(gs) - action = dict([(x, gs.players[x[0]] / tr) for x in choices]) + action = dict( + [(x, gs.players[x[0]] / tr) for x in choices if x != "Detective Out"] + ) return action if gs.time == 1: choices = [x for x in mafia.night_outcomes(gs).keys()] diff --git a/test_mafia.py b/test_mafia.py index 251b10c..cace928 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -66,3 +66,16 @@ def test_leaves_match_children(): 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 = simple_strat(mgs) + cgs_outcomes = simple_strat(cgs) + dgs_outcomes = simple_strat(dgs) + assert sum(mgs_outcomes.values()) == 1 + assert sum(cgs_outcomes.values()) == 1 + assert sum(dgs_outcomes.values()) == 1 From 46a97b66be4be651015e5372445dca9f9dba0c57 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 08:02:51 -0700 Subject: [PATCH 12/26] Marked the two failing tests as skipped --- test_mafia.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test_mafia.py b/test_mafia.py index cace928..a3e29c2 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -1,3 +1,5 @@ +import pytest + import mafia from main import new_game, simple_strat @@ -40,6 +42,7 @@ def test_simple_strat_original_strat_same(): assert weight_dict_original == weight_dict_simple +@pytest.mark.skip(reason="Aspirational test") def test_can_play_detective(): pl, gs, games = new_game(2, 18, 1, 0) weight_dict = mafia.eval_strat_rc(games, simple_strat) @@ -66,6 +69,7 @@ def test_leaves_match_children(): assert len(targets) == 1 +@pytest.mark.skip(reason="Aspirational test") def test_strategy_with_detective_sum_is_one(): pl, gs, games = new_game(2, 4, 1, 0) mgs, cgs, dgs = [ From 7dfa50e83b7fbf7ab57bd5dc78ef346848e6534e Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 14:00:37 -0700 Subject: [PATCH 13/26] Moved to using fractions instead of floats --- mafia.py | 5 +++-- main.py | 9 +++++++-- test_mafia.py | 10 ++++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/mafia.py b/mafia.py index 485a53f..7604ff3 100644 --- a/mafia.py +++ b/mafia.py @@ -2,6 +2,7 @@ import typing import enum import collections +from fractions import Fraction class PType(enum.IntEnum): @@ -591,7 +592,7 @@ def apply_strat(game, node, day, fstrat, weight_dict): if node.is_leaf(): return elif nid == ("root" + str(node.data[1].day - 1)): - weight_dict["root" + str(day)] = 1.0 + weight_dict["root" + str(day)] = Fraction(1, 1) if day > 0: return outcomes = fstrat(node.data[1]) @@ -603,7 +604,7 @@ def apply_strat(game, node, day, fstrat, weight_dict): def eval_strat_rc(games, fstrat): - weight_dict = collections.defaultdict(float) + 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) diff --git a/main.py b/main.py index f70c5a9..e76bf03 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ import mafia import collections +from fractions import Fraction def simple_strat(gs): @@ -8,13 +9,17 @@ def simple_strat(gs): choices = [x for x in mafia.day_outcomes(gs).keys()] tr = mafia.total_remaining(gs) action = dict( - [(x, gs.players[x[0]] / tr) for x in choices if x != "Detective Out"] + [ + (x, Fraction(gs.players[x[0]], tr)) + for x in choices + if x != "Detective Out" + ] ) return action if gs.time == 1: choices = [x for x in mafia.night_outcomes(gs).keys()] tr = mafia.citizens_remaining(gs) - action = dict([(x, gs.players[x[0]] / tr) for x in choices]) + action = dict([(x, Fraction(gs.players[x[0]], tr)) for x in choices]) return action diff --git a/test_mafia.py b/test_mafia.py index a3e29c2..80076c0 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -1,3 +1,5 @@ +from fractions import Fraction + import pytest import mafia @@ -9,12 +11,12 @@ def original_strat(gs): if gs.time == 0: choices = [x for x in mafia.day_outcomes(gs).keys()] tr = mafia.total_remaining(gs) - action = dict([(x, gs.players[x[0]] / tr) for x in choices]) + 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, gs.players[x[0]] / tr) for x in choices]) + action = dict([(x, Fraction(gs.players[x[0]], tr)) for x in choices]) return action @@ -31,8 +33,8 @@ def test_nothing(): def test_winner_probabilities(): pl, gs, games, weight_dict = original_game() mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) - assert mafia_win == 0.49290131952670657 - assert citizen_win == 0.5070986804732934 + assert mafia_win == Fraction(478099, 969969) + assert citizen_win == Fraction(491870, 969969) def test_simple_strat_original_strat_same(): From d58defdde24d399a02d154fc55c4ac4743bca201 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 14:19:04 -0700 Subject: [PATCH 14/26] Can run a game with a detective, though only if the detective does nothing useful --- main.py | 118 ++++++++++++++++++++++++++++++++++++++++++++++++-- test_mafia.py | 12 ++++- 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index e76bf03..2dbe4d3 100644 --- a/main.py +++ b/main.py @@ -17,12 +17,118 @@ def simple_strat(gs): ) 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]) + # 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 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: + assert 1 == 0 + 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 @@ -32,7 +138,13 @@ def new_game(num_mafia, num_citizen, num_detective, num_bodyguard): return pl, gs, games +dpl, dgs, dgame = new_game(2, 5, 1, 0) +cs = get_all_choices(dgame) + pl, gs, games = new_game(2, 19, 0, 0) weight_dict = mafia.eval_strat_rc(games, simple_strat) mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) print(mafia_win, citizen_win) + +igs = get_all_incomplete_gs(dgame) +strat_out = [simple_strat(i) for i in igs] diff --git a/test_mafia.py b/test_mafia.py index 80076c0..196a297 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -44,7 +44,6 @@ def test_simple_strat_original_strat_same(): assert weight_dict_original == weight_dict_simple -@pytest.mark.skip(reason="Aspirational test") def test_can_play_detective(): pl, gs, games = new_game(2, 18, 1, 0) weight_dict = mafia.eval_strat_rc(games, simple_strat) @@ -71,7 +70,6 @@ def test_leaves_match_children(): assert len(targets) == 1 -@pytest.mark.skip(reason="Aspirational test") def test_strategy_with_detective_sum_is_one(): pl, gs, games = new_game(2, 4, 1, 0) mgs, cgs, dgs = [ @@ -85,3 +83,13 @@ def test_strategy_with_detective_sum_is_one(): assert sum(mgs_outcomes.values()) == 1 assert sum(cgs_outcomes.values()) == 1 assert sum(dgs_outcomes.values()) == 1 + + +def test_useless_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, simple_strat) + assert mafia.winner_probabilities(games, gwd) == mafia.winner_probabilities( + dgames, dwd + ) From da348d72b80d2108131c198143291c5368680b95 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 15:44:00 -0700 Subject: [PATCH 15/26] Moved strategies to their own file --- main.py | 67 +---------------------------------------- strategies.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++ test_mafia.py | 15 +--------- 3 files changed, 85 insertions(+), 80 deletions(-) create mode 100644 strategies.py diff --git a/main.py b/main.py index 2dbe4d3..9693825 100644 --- a/main.py +++ b/main.py @@ -2,71 +2,7 @@ import collections from fractions import Fraction - -def simple_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 - 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 +from strategies import simple_strat def get_all_incomplete_gs(games): @@ -128,7 +64,6 @@ def unique_choices(choices, before_phase=(0, 1)): 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 diff --git a/strategies.py b/strategies.py new file mode 100644 index 0000000..1d66df3 --- /dev/null +++ b/strategies.py @@ -0,0 +1,83 @@ +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 simple_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 + 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 diff --git a/test_mafia.py b/test_mafia.py index 196a297..77d0487 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -4,20 +4,7 @@ import mafia from main import new_game, simple_strat - - -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 +from strategies import original_strat def original_game(): From eadc843518fc23943a917e0290db914081d1134e Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 15:50:00 -0700 Subject: [PATCH 16/26] Renamed simple_strat to incomplete_detective --- main.py | 6 +++--- strategies.py | 2 +- test_mafia.py | 18 +++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/main.py b/main.py index 9693825..8c91325 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ import collections from fractions import Fraction -from strategies import simple_strat +from strategies import incomplete_detective def get_all_incomplete_gs(games): @@ -77,9 +77,9 @@ def new_game(num_mafia, num_citizen, num_detective, num_bodyguard): cs = get_all_choices(dgame) pl, gs, games = new_game(2, 19, 0, 0) -weight_dict = mafia.eval_strat_rc(games, simple_strat) +weight_dict = mafia.eval_strat_rc(games, incomplete_detective) mafia_win, citizen_win = mafia.winner_probabilities(games, weight_dict) print(mafia_win, citizen_win) igs = get_all_incomplete_gs(dgame) -strat_out = [simple_strat(i) for i in igs] +strat_out = [incomplete_detective(i) for i in igs] diff --git a/strategies.py b/strategies.py index 1d66df3..1676a5e 100644 --- a/strategies.py +++ b/strategies.py @@ -17,7 +17,7 @@ def original_strat(gs): return action -def simple_strat(gs): +def incomplete_detective(gs): # no detectives if gs.time == 0: choices = [x for x in mafia.day_outcomes(gs).keys()] diff --git a/test_mafia.py b/test_mafia.py index 77d0487..6a355a7 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -3,7 +3,7 @@ import pytest import mafia -from main import new_game, simple_strat +from main import new_game, incomplete_detective from strategies import original_strat @@ -24,16 +24,16 @@ def test_winner_probabilities(): assert citizen_win == Fraction(491870, 969969) -def test_simple_strat_original_strat_same(): +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, simple_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, simple_strat) + 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 @@ -64,19 +64,19 @@ def test_strategy_with_detective_sum_is_one(): for x in games[0].children(games[0].root) if x.data[0] != "Detective Out" ] - mgs_outcomes = simple_strat(mgs) - cgs_outcomes = simple_strat(cgs) - dgs_outcomes = simple_strat(dgs) + 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_useless_detective_same_as_no_detective(): +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, simple_strat) + dwd = mafia.eval_strat_rc(dgames, incomplete_detective) assert mafia.winner_probabilities(games, gwd) == mafia.winner_probabilities( dgames, dwd ) From b1ce7c62d86c97cb1336be8fe53d64644557f228 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 16:04:36 -0700 Subject: [PATCH 17/26] Updated docstring for incomplete detective, added currently not working proper_detective and aspirational test --- strategies.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++- test_mafia.py | 22 +++++++++++++-- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/strategies.py b/strategies.py index 1676a5e..1a315c9 100644 --- a/strategies.py +++ b/strategies.py @@ -18,7 +18,83 @@ def original_strat(gs): def incomplete_detective(gs): - # no detectives + """ + This detective is incomplete in two ways + 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 proper_detective(gs): + """ + I copied incomplete detective so I could work on this. Despite + the name of the function, it's still incomplete. + """ if gs.time == 0: choices = [x for x in mafia.day_outcomes(gs).keys()] tr = mafia.total_remaining(gs) diff --git a/test_mafia.py b/test_mafia.py index 6a355a7..2781622 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -3,8 +3,8 @@ import pytest import mafia -from main import new_game, incomplete_detective -from strategies import original_strat +from main import new_game +from strategies import original_strat, incomplete_detective, proper_detective def original_game(): @@ -80,3 +80,21 @@ def test_incomplete_detective_same_as_no_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 + + +@pytest.mark.skip(reason="Aspirational") +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 From 33a763bfaed1572440e38ad89285c1f2e852e2f1 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 17:10:37 -0700 Subject: [PATCH 18/26] Added a test for situation where detective comes out, then continues peeking --- test_mafia.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test_mafia.py b/test_mafia.py index 2781622..6ae2cfe 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -3,7 +3,7 @@ import pytest import mafia -from main import new_game +from main import new_game, get_all_incomplete_gs from strategies import original_strat, incomplete_detective, proper_detective @@ -98,3 +98,16 @@ def test_proper_detective_beats_incomplete_detective(): 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 From d999d4752c56bdecb1773a07c13219c2e7b4b9c8 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 17:11:51 -0700 Subject: [PATCH 19/26] Forced all detective peeks to become verified if the detective is already verified --- mafia.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mafia.py b/mafia.py index 7604ff3..ba7e8ea 100644 --- a/mafia.py +++ b/mafia.py @@ -155,6 +155,10 @@ 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: """1 = town, 0 = not over, -1 = mafia""" mr = mafia_remaining(gs) @@ -445,6 +449,7 @@ def night_outcomes(gs: Gamestate): ] = Gamestate(gs.day + 1, 0, bg_protect, new_players) 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(): @@ -456,6 +461,17 @@ def night_outcomes(gs: Gamestate): 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)), + ) return outcomes From 5aff75860480765e43a0c6419f79ca2d4f5a454d Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 17:47:05 -0700 Subject: [PATCH 20/26] Partially implemented proper detective -- enough that citizens win more often now --- main.py | 14 ++++++++++++-- strategies.py | 53 +++++++++++++++++++++++++++++++++++++++++---------- test_mafia.py | 1 - 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/main.py b/main.py index 8c91325..4be3ee0 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ import collections from fractions import Fraction -from strategies import incomplete_detective +from strategies import incomplete_detective, proper_detective def get_all_incomplete_gs(games): @@ -73,7 +73,12 @@ def new_game(num_mafia, num_citizen, num_detective, num_bodyguard): return pl, gs, games -dpl, dgs, dgame = new_game(2, 5, 1, 0) +def frac_to_pct(frac): + rf = round(frac, 4) + return rf.numerator / rf.denominator + + +dpl, dgs, dgame = new_game(2, 18, 1, 0) cs = get_all_choices(dgame) pl, gs, games = new_game(2, 19, 0, 0) @@ -83,3 +88,8 @@ def new_game(num_mafia, num_citizen, num_detective, num_bodyguard): igs = get_all_incomplete_gs(dgame) strat_out = [incomplete_detective(i) for i in igs] + +# game states where a mafia has been peeked and it's night time +peekgs = [i for i in igs if i.players[mafia.PType.PeekedMafia] > 0 and i.time == 0] + +mafia.eval_strat_rc(dgame, proper_detective) diff --git a/strategies.py b/strategies.py index 1a315c9..5981cbf 100644 --- a/strategies.py +++ b/strategies.py @@ -90,21 +90,54 @@ def incomplete_detective(gs): 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) + has_peeks = gs.players[mafia.PType.PeekedMafia] > 0 + is_day = gs.time == 0 + return all([has_detective, has_peeks, is_day]) + + def proper_detective(gs): """ - I copied incomplete detective so I could work on this. Despite - the name of the function, it's still incomplete. + 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 (todo) + 4. If there's a VerifiedDetective, mafia will always assasinate (todo) + 5. If there's a VerifiedCitizen, and detective is dead, mafia will always assasinate (todo) """ 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" - ] - ) + 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: + tr = mafia.total_remaining(gs) + for x in choices: + if x == "Detective Out": + # Never come out + action[x] = Fraction() + else: + action[x] = Fraction(gs.players[x[0]], tr) return action if gs.time == 1: # get all the choices for this round diff --git a/test_mafia.py b/test_mafia.py index 6ae2cfe..9dd4aba 100644 --- a/test_mafia.py +++ b/test_mafia.py @@ -89,7 +89,6 @@ def test_can_play_proper_detective(): assert mafia_win + citizen_win == 1 -@pytest.mark.skip(reason="Aspirational") def test_proper_detective_beats_incomplete_detective(): _, _, games = new_game(2, 18, 1, 0) idwd = mafia.eval_strat_rc(games, incomplete_detective) From ec3d8e5d9f869c7ad6bab40664f9a488aa9ab7e8 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 17:56:37 -0700 Subject: [PATCH 21/26] Ensure all actions add up to 1 in the day time, too --- strategies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/strategies.py b/strategies.py index 5981cbf..90eabec 100644 --- a/strategies.py +++ b/strategies.py @@ -138,6 +138,7 @@ def proper_detective(gs): action[x] = Fraction() else: action[x] = Fraction(gs.players[x[0]], tr) + assert sum(action.values()) == 1 return action if gs.time == 1: # get all the choices for this round From 020b1e1150da0fbea1aa77059d280c9667cbd40e Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 17:56:54 -0700 Subject: [PATCH 22/26] Clean up main.py ever so slightly so it's better for exploring --- main.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/main.py b/main.py index 4be3ee0..cfce144 100644 --- a/main.py +++ b/main.py @@ -78,18 +78,24 @@ def frac_to_pct(frac): return rf.numerator / rf.denominator -dpl, dgs, dgame = new_game(2, 18, 1, 0) -cs = get_all_choices(dgame) +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) + print(frac_to_pct(mafia_win), frac_to_pct(citizen_win)) -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) -print(mafia_win, citizen_win) -igs = get_all_incomplete_gs(dgame) -strat_out = [incomplete_detective(i) for i in igs] +def example_with_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) + print(mafia_win, citizen_win) -# game states where a mafia has been peeked and it's night time -peekgs = [i for i in igs if i.players[mafia.PType.PeekedMafia] > 0 and i.time == 0] -mafia.eval_strat_rc(dgame, proper_detective) +pl, gs, games = new_game(2, 4, 1, 0) +choices = get_all_choices(games) +igs = get_all_incomplete_gs(games) +strat_decisions = [proper_detective(i) for i in igs] + +# game states where a mafia has been peeked and it's night time +# peekgs = [i for i in igs if i.players[mafia.PType.PeekedMafia] > 0 and i.time == 0] From 1e54a8ba646de157fe53daadda101821abbe3620 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 19:17:37 -0700 Subject: [PATCH 23/26] Add checks for is_suspicious in order to avoid Verified safe --- mafia.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/mafia.py b/mafia.py index ba7e8ea..04cafcf 100644 --- a/mafia.py +++ b/mafia.py @@ -67,6 +67,10 @@ 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) @@ -147,6 +151,22 @@ 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 From e62acf1599b532b4c5c2cea0e910ebc20e1dc644 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 19:18:23 -0700 Subject: [PATCH 24/26] Fully implement "proper detective" --- main.py | 34 +++++++++++++++++++------ strategies.py | 70 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 81 insertions(+), 23 deletions(-) diff --git a/main.py b/main.py index cfce144..478c166 100644 --- a/main.py +++ b/main.py @@ -82,20 +82,40 @@ 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) - print(frac_to_pct(mafia_win), frac_to_pct(citizen_win)) + # ~0.4929 0.5071 + print("No Detective:", frac_to_pct(mafia_win), frac_to_pct(citizen_win)) -def example_with_detective(): +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) - print(mafia_win, citizen_win) + # ~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) -strat_decisions = [proper_detective(i) for i in igs] - -# game states where a mafia has been peeked and it's night time -# peekgs = [i for i in igs if i.players[mafia.PType.PeekedMafia] > 0 and i.time == 0] +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 index 90eabec..3d01753 100644 --- a/strategies.py +++ b/strategies.py @@ -105,9 +105,11 @@ 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 (todo) - 4. If there's a VerifiedDetective, mafia will always assasinate (todo) - 5. If there's a VerifiedCitizen, and detective is dead, mafia will always assasinate (todo) + 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 (todo) + 6. If there's a VerifiedCitizen, and no VerifiedDetective, mafia will always assasinate (todo) + 7. Correctly assign random chances to the case when mafia assasinate and detective peek the same type (todo) """ if gs.time == 0: choices = [x for x in mafia.day_outcomes(gs).keys()] @@ -131,13 +133,19 @@ def proper_detective(gs): else: action[c] = Fraction(0, 1) else: - tr = mafia.total_remaining(gs) + # 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: - action[x] = Fraction(gs.players[x[0]], tr) + 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: @@ -148,12 +156,30 @@ def proper_detective(gs): 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 + + # 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 = {} - 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) + # 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 @@ -183,11 +209,23 @@ def proper_detective(gs): # 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] + # 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 From 55610a4735ea9a1a4e4c29d4ea665a7b22860630 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Sun, 18 Apr 2021 19:23:26 -0700 Subject: [PATCH 25/26] Amended some comments --- strategies.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/strategies.py b/strategies.py index 3d01753..58da853 100644 --- a/strategies.py +++ b/strategies.py @@ -19,12 +19,15 @@ def original_strat(gs): def incomplete_detective(gs): """ - This detective is incomplete in two ways + 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()] From 8e878a180b2c8a366f8b4f3b8d525d6dd073e784 Mon Sep 17 00:00:00 2001 From: hopefulwerewolf <80228084+hopefulwerewolf@users.noreply.github.com> Date: Mon, 19 Apr 2021 20:26:47 -0700 Subject: [PATCH 26/26] Fixed some more comments and made an explicit exception in a case that shouldn't happen --- main.py | 2 +- strategies.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 478c166..3249dbd 100644 --- a/main.py +++ b/main.py @@ -40,7 +40,7 @@ def get_all_choices(games): for k, v in mafia.night_outcomes(node.data[1]).items(): choices.append({"before": node.data[1], "choice": k, "after": v}) else: - assert 1 == 0 + raise Exception("This case should not happen") return choices diff --git a/strategies.py b/strategies.py index 58da853..70708b7 100644 --- a/strategies.py +++ b/strategies.py @@ -99,9 +99,12 @@ def is_time_to_come_out(gs): 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, has_peeks, is_day]) + return all([has_detective, detective_not_already_out, has_peeks, is_day]) def proper_detective(gs): @@ -110,9 +113,9 @@ def proper_detective(gs): 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 (todo) - 6. If there's a VerifiedCitizen, and no VerifiedDetective, mafia will always assasinate (todo) - 7. Correctly assign random chances to the case when mafia assasinate and detective peek the same type (todo) + 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()]