EV Calculator Program

This forum is for discussion related to the game.
User avatar
Something_Smart
Something_Smart
He/him
Somewhat_Balanced
User avatar
User avatar
Something_Smart
He/him
Somewhat_Balanced
Somewhat_Balanced
Posts: 23122
Joined: November 17, 2015
Pronoun: He/him
Location: Upstate New York

EV Calculator Program

Post Post #0 (ISO) » Tue Sep 22, 2020 8:24 am

Post by Something_Smart »

Figured I'd post this in case anyone else wants to mess around with it. Just a little EV calculator program I wrote to test some setups.
Spoiler:

Code: Select all

import sys

class Setup:
    def __init__(self, playerlist):
        self.town = len([c for c in playerlist if not c])
        self.scum = len([c for c in playerlist if c])
        self.playerlist = playerlist
        self.phase = 0
        self.victory = 0

    def calculate(self):
        while not self.check_victory():
            self.phase += 1
            self.elimination()
            self.night_actions()
        if self.check_victory() == 1:
            return True
        else:
            return False

    def elimination(self):
        self.remove_player(len(self.playerlist) - 1)

    def nightkill(self):
        idx = 0
        while self.playerlist[idx]:
            idx += 1
        self.remove_player(idx)

    def night_actions(self):
        self.nightkill()

    def remove_player(self, idx):
        align = self.playerlist.pop(idx)
        if align:
            self.scum -= 1
            return 1
        else:
            self.town -= 1
            return 0

    def check_victory(self):
        if self.scum >= self.town or self.victory == 1:
            return 1
        elif self.scum == 0 or self.victory == -1:
            return -1
        else:
            return 0

class Mountainous(Setup):
    pass

class WhiteFlag(Setup):
    def check_victory(self):
        if self.scum >= self.town:
            return 1
        elif self.scum <= 1:
            return -1
        else:
            return 0

class Nightless(Setup):
    def night_actions(self):
        pass

class TrustFall(Nightless):
    def __init__(self, playerlist):
        Nightless.__init__(self, playerlist)
        self.auto = False

    def elimination(self):
        if self.playerlist[0] == self.playerlist[1] == True:
            self.auto = True
        self.remove_player(0)
        self.remove_player(0)

    def check_victory(self):
        if self.auto:
            return -1
        if self.scum >= self.town:
            return -1
        elif self.scum == 0:
            return 1
        else:
            return 0

class GoldenSun(Setup):
    def elimination(self):
        if self.phase <= 4:
            align = self.remove_player(len(self.playerlist) - 1)
            if align:
                Setup.night_actions(self)
        else:
            self.victory = -1
            for j in range(len(self.playerlist) - self.scum, len(self.playerlist)):
                if not self.playerlist[j]:
                    self.victory = 1

    def night_actions(self):
        if self.phase == 2:
            Setup.night_actions(self)

class GoldenSunV2(Setup):
    def elimination(self):
        if self.town > 4:
            align = self.remove_player(len(self.playerlist) - 1)
            if align:
                Setup.night_actions(self)
        else:
            self.victory = -1
            for j in range(len(self.playerlist) - self.scum, len(self.playerlist)):
                if not self.playerlist[j]:
                    self.victory = 1

    def night_actions(self):
        pass

def calculate_ev(setup_type, town_count, scum_count):
    permutations = permute(scum_count, town_count)
    total_cases = len(permutations)
    town_wins = 0.0
    for p in permutations:
        s = setup_type(p)
        res = s.calculate()
        if not res:
            town_wins += 1

    return town_wins / total_cases


def permute(trues, falses):
    res = []

    if trues == 0:
        res.append([])
        for x in range(falses):
            res[0].append(False)
        return res
    if falses == 0:
        res.append([])
        for x in range(trues):
            res[0].append(True)
        return res

    nw = permute(trues - 1, falses)
    for l in nw:
        res.append([True] + l)

    nw = permute(trues, falses - 1)
    for l in nw:
        res.append([False] + l)
    return res

def main():
    print(calculate_ev(TrustFall, 8, 2))

if __name__ == "__main__":
    if len(sys.argv) < 4:
        main()
    else:
        setup_str = sys.argv[1]
        setup = getattr(sys.modules[__name__], setup_str)
        town_count = int(sys.argv[2])
        scum_count = int(sys.argv[3])
        print(calculate_ev(setup, town_count, scum_count))

If you have Python on your computer, you should just be able to paste it in and run it; otherwise, you can use a website such as this to run it.

You can either run it with command line arguments [setupname] [#town] [#scum], or just go in and edit line 155 ("print(calculate_ev(TrustFall, 8, 2))") with the appropriate setup name, town count, and scum count.

It uses the Charisma Model of EV Calculation, which assumes that players are randomly ordered from towniest to scummiest, and scummiest players are eliminated while towniest townies are NK'd.

Current setups supported are Mountainous, White Flag, Nightless, Trust Fall, and a couple versions of Isis's Golden Sun. So far it doesn't have support for PR's; I could add it, but I don't really feel like EV is that useful for PR setups. Maybe very simple ones.

To add another setup, just create a new class like the others and override whichever methods are different:
  • calculate - for the overall day/night structure
  • elimination - for determining who gets eliminated and anything else that happens during the day
  • nightkill - for determining who, if anyone, dies during the night
  • check_victory - for determining who won; 1 if scum won, -1 if town won, 0 if the game is still going.
  • (remove_player handles the death of a particular slot, but there shouldn't be a need to override this.)
The living players are stored in playerlist, with the towniest players at the front of the list and the scummiest players at the back; the list contains True if the player is scum, and False if they are town.

If you have any questions, let me know.
It's always the same. When you fire that first shot, no matter how right you feel, you have no idea who's going to die. You don't know whose children are going to scream and burn. How many hearts will be broken. How many lives shattered. How much blood will spill, until everybody does what they're always going to have to do from the very beginning... SIT DOWN AND TALK!
User avatar
Ythan
Ythan
She
Welcome to the Haystack
User avatar
User avatar
Ythan
She
Welcome to the Haystack
Welcome to the Haystack
Posts: 15149
Joined: August 11, 2009
Pronoun: She

Post Post #1 (ISO) » Tue Sep 22, 2020 9:16 am

Post by Ythan »

Oh I like that charisma model.

Also this cool program, that just really caught my eye and I hadn't seen it before.
User avatar
Something_Smart
Something_Smart
He/him
Somewhat_Balanced
User avatar
User avatar
Something_Smart
He/him
Somewhat_Balanced
Somewhat_Balanced
Posts: 23122
Joined: November 17, 2015
Pronoun: He/him
Location: Upstate New York

Post Post #2 (ISO) » Sun Sep 27, 2020 7:10 am

Post by Something_Smart »

Updated version.

Spoiler:

Code: Select all

import sys

class Setup:
    def __init__(self, playerlist):
        self.town = len([c for c in playerlist if not c])
        self.scum = len([c for c in playerlist if c])
        self.playerlist = playerlist
        self.phase = 0
        self.victory = 0

    def calculate(self):
        while not self.check_victory():
            self.phase += 1
            self.elimination()
            self.night_actions()
        if self.check_victory() == 1:
            return 0
        else:
            return 1

    def elimination(self):
        self.remove_player(len(self.playerlist) - 1)

    def nightkill(self):
        idx = 0
        while self.playerlist[idx] >= 1:
            idx += 1
        return self.remove_player(idx)

    def night_actions(self):
        self.nightkill()

    def remove_player(self, idx):
        align = self.playerlist.pop(idx)
        if align >= 1:
            self.scum -= 1
        else:
            self.town -= 1
        return align

    def move_to_position(self, starting, ending):
        val = self.playerlist.pop(starting)
        if starting < ending:
            self.playerlist.insert(ending - 1, val)
        else:
            self.playerlist.insert(ending, val)

    def check_victory(self):
        if self.scum >= self.town or self.victory == 1:
            return 1
        elif self.scum == 0 or self.victory == -1:
            return -1
        else:
            return 0

class Mountainous(Setup):
    pass

class WhiteFlag(Setup):
    def check_victory(self):
        if self.scum >= self.town:
            return 1
        elif self.scum <= 1:
            return -1
        else:
            return 0

class Nightless(Setup):
    def night_actions(self):
        pass

class TrustFall(Nightless):
    def __init__(self, playerlist):
        Nightless.__init__(self, playerlist)
        self.auto = False

    def elimination(self):
        if self.playerlist[0] == self.playerlist[1] == True:
            self.auto = True
        self.remove_player(0)
        self.remove_player(0)

    def check_victory(self):
        if self.auto:
            return -1
        if self.scum >= self.town:
            return -1
        elif self.scum == 0:
            return 1
        else:
            return 0

class GoldenSun(Setup):
    def elimination(self):
        if self.phase <= 4:
            align = self.remove_player(len(self.playerlist) - 1)
            if align:
                Setup.night_actions(self)
        else:
            self.victory = -1
            for j in range(len(self.playerlist) - self.scum, len(self.playerlist)):
                if not self.playerlist[j]:
                    self.victory = 1

    def night_actions(self):
        if self.phase == 2:
            Setup.night_actions(self)

class GoldenSunV2(Setup):
    def elimination(self):
        if self.town > 4:
            align = self.remove_player(len(self.playerlist) - 1)
            if align:
                Setup.night_actions(self)
        else:
            self.victory = -1
            for j in range(len(self.playerlist) - self.scum, len(self.playerlist)):
                if not self.playerlist[j]:
                    self.victory = 1

    def night_actions(self):
        pass

class Vengescum(Setup):
    def elimination(self):
        align = self.remove_player(len(self.playerlist) - 1)
        if align:
            Setup.night_actions(self)

    def night_actions(self):
        pass

class DesperationDay(Setup):
    def elimination(self):
        align = self.remove_player(len(self.playerlist) - 1)
        if align and self.phase == 3:
            self.victory = -1

class DoubleDay(Setup):
    def elimination(self):
        Setup.elimination(self)
        if not self.check_victory():
            Setup.elimination(self)

class GreyFlagNightless(Vengescum, WhiteFlag):
    pass

class NightlessVengefulMayhem(Setup):
    def night_actions(self):
        if self.phase == 2 or self.phase == 3:
            Setup.night_actions(self)

class PRSetup(Setup):
    def __init__(self, playerlist, pr_count):
        Setup.__init__(self, playerlist)
        self.pr_count = pr_count

    def calculate(self):
        town_wins = 0
        pr_arrangements = permute(self.pr_count, self.town - self.pr_count)
        ordering = self.playerlist + []
        for s in pr_arrangements:
            Setup.__init__(self, ordering + [])
            index = 0
            for j in range(len(self.playerlist)):
                if self.playerlist[j] == 0:
                    if s[index] == 1:
                        self.playerlist[j] = -1
                    index += 1
            while self.check_victory() == 0:
                self.phase += 1
                self.elimination()
                self.night_actions()
            if self.check_victory() != 1:
                town_wins += 1
        return town_wins / len(pr_arrangements)

class Science(PRSetup):
    def __init__(self, playerlist, pr_count=2):
        PRSetup.__init__(self, playerlist, pr_count)

    def elimination(self):
        while self.playerlist[-1] == -1:
            self.move_to_position(len(self.playerlist) - 1, 0)
        self.remove_player(len(self.playerlist) - 1)

class Cop9er(PRSetup):
    def __init__(self, playerlist, pr_count=1):
        PRSetup.__init__(self, playerlist, pr_count)

    def cop_claim(self):
        inno_indices = [i for i in range(len(self.playerlist)) if self.playerlist[i] == -2]
        for i in inno_indices:
            self.move_to_position(i, 0)
        if -1 in self.playerlist:
            self.move_to_position(self.playerlist.index(-1), 0)
        if 2 in self.playerlist:
            self.move_to_position(self.playerlist.index(2), len(self.playerlist))

    def elimination(self):
        if -1 in self.playerlist:
            if 2 in self.playerlist or self.playerlist[-1] <= -1:
                self.cop_claim()
        PRSetup.elimination(self)

    def night_actions(self):
        if -1 in self.playerlist and self.check_victory() == 0 and self.playerlist[0] != -1:
            check = int(len(self.playerlist) / 2)
            while self.playerlist[check] == -2 or self.playerlist[check] == -1:
                check -= 1
            if self.playerlist[check] == 0:
                # Inno
                self.playerlist[check] = -2
            elif self.playerlist[check] == 1:
                # Guilty
                self.playerlist[check] = 2
        align = self.nightkill()
        if align == -1:
            self.cop_claim()

def calculate_ev(setup_type, town_count, scum_count):
    permutations = permute(scum_count, town_count)
    total_cases = len(permutations)
    town_wins = 0.0
    for p in permutations:
        s = setup_type(p)
        res = s.calculate()
        town_wins += res

    return town_wins / total_cases

def permute(trues, falses):
    res = []

    if trues == 0:
        res.append([])
        for x in range(falses):
            res[0].append(0)
        return res
    if falses == 0:
        res.append([])
        for x in range(trues):
            res[0].append(1)
        return res

    nw = permute(trues - 1, falses)
    for l in nw:
        res.append([1] + l)

    nw = permute(trues, falses - 1)
    for l in nw:
        res.append([0] + l)
    return res

def main():
    print(calculate_ev(Cop9er, 7, 2))

if __name__ == "__main__":
    if len(sys.argv) < 4:
        main()
    else:
        setup_str = sys.argv[1]
        setup = getattr(sys.modules[__name__], setup_str)
        town_count = int(sys.argv[2])
        scum_count = int(sys.argv[3])
        print(calculate_ev(setup, town_count, scum_count))


I happened to be working on some setups for the Approved Open Setup discussion, and then I felt like modifying this so it could support simple PR play.

It's now possible to have a setup with any number of copies of a single town PR. To do this, extend PRSetup and override __init__ as demonstrated to set the number of PR's. In the playerlist variable, VT's are 0, scum are 1, and town PR's are -1 by default, but you can play with the numbers as long as you keep scum values >= 1 and town values <= 0. (In the Cop 9er, I used -2 to represent townies that were cleared by the cop and 2 to represent scum that were guiltied.)

The following setups have been added: Vengescum (nightless but scum are vengeful; No Eliminating Town is equivalent to this), Desperation Day, Double Day, Grey Flag Nightless (TIL that python has multiple inheritance), Nightless Vengeful Mayhem, SCIENCE! (2 goons, 2 masons, 3 VT's), and Cop 9er (2 goons, 1 cop, 6 VT's).

For pretty much any setup with PR's, some simplifying assumptions will need to be made. I've been using the following assumptions: scum never claim PR, scum can't PR hunt with better than random accuracy, cops check in the middle of the playerlist, cops claim only if they have a guilty or if they or an inno are about to be executed, and cops can crumb their results perfectly. These assumptions significantly decrease the objectivity of the result (since you can EV-hack by changing your assumptions) but the result is still useful as a ballpark estimate.
It's always the same. When you fire that first shot, no matter how right you feel, you have no idea who's going to die. You don't know whose children are going to scream and burn. How many hearts will be broken. How many lives shattered. How much blood will spill, until everybody does what they're always going to have to do from the very beginning... SIT DOWN AND TALK!
User avatar
Something_Smart
Something_Smart
He/him
Somewhat_Balanced
User avatar
User avatar
Something_Smart
He/him
Somewhat_Balanced
Somewhat_Balanced
Posts: 23122
Joined: November 17, 2015
Pronoun: He/him
Location: Upstate New York

Post Post #3 (ISO) » Sun Sep 27, 2020 7:17 am

Post by Something_Smart »

Hmm, after rereading Mathdino's thread, it would be pretty easy to assume that scum will always claim PR if they're about to be executed. Let me try that.
It's always the same. When you fire that first shot, no matter how right you feel, you have no idea who's going to die. You don't know whose children are going to scream and burn. How many hearts will be broken. How many lives shattered. How much blood will spill, until everybody does what they're always going to have to do from the very beginning... SIT DOWN AND TALK!
User avatar
Something_Smart
Something_Smart
He/him
Somewhat_Balanced
User avatar
User avatar
Something_Smart
He/him
Somewhat_Balanced
Somewhat_Balanced
Posts: 23122
Joined: November 17, 2015
Pronoun: He/him
Location: Upstate New York

Post Post #4 (ISO) » Sun Sep 27, 2020 7:20 am

Post by Something_Smart »

Whoa, allowing scum to claim cop and assuming that the real cop always outs to execute the fake cop causes the EV to drop from 45% to 31%. This is what I mean about EV-hacking.
It's always the same. When you fire that first shot, no matter how right you feel, you have no idea who's going to die. You don't know whose children are going to scream and burn. How many hearts will be broken. How many lives shattered. How much blood will spill, until everybody does what they're always going to have to do from the very beginning... SIT DOWN AND TALK!
User avatar
the worst
the worst
Snuggly Duckling
User avatar
User avatar
the worst
Snuggly Duckling
Snuggly Duckling
Posts: 36602
Joined: November 7, 2015
Location: pond

Post Post #5 (ISO) » Sun Sep 27, 2020 5:50 pm

Post by the worst »

how am i so late to this
ego
who's scum? i haven't read up yet but like, it's me
--
intermittent v/la until late march
User avatar
Psyche
Psyche
he/they
Survivor
User avatar
User avatar
Psyche
he/they
Survivor
Survivor
Posts: 10662
Joined: April 28, 2011
Pronoun: he/they

Post Post #6 (ISO) » Tue Sep 29, 2020 8:47 am

Post by Psyche »

can stick this in a google colab notebook to improve accessibility
Post Reply

Return to “Mafia Discussion”