# monty_hall_problem.py - Monthy Hall Problem simulation
#
# Copyright (C) 2015  Flying Stone Technology 
# Author: NIIBE Yutaka <gniibe@fsij.org>
#
# This program is licensed under GPLv3+.

# In this program, door is encoded as: 0, 1, 2


from random import randint, choice, seed

# Random-Bit-Generator (RBG) and its access control is very important.
# This class models RBG and its possible side channel.
# Suppose that we need administrative information for this RBG and
# have a counter which shows how many bits are generated.
class rbg(object):
    def __init__(self):
        self.fix_012 = True
        self.counter = 0

    def access_rbg(self,bits):
        self.counter += bits

    def examine(self):
        return self.counter

    def random_number_012(self):
        self.access_rbg(2)
        if not self.fix_012:
            return randint(0,3) % 3  # Naive implementation: get bits and MOD
        else:
            return randint(0,2)

    def random_number_0_or_1(self):
        self.access_rbg(1)
        return choice([1,2])

    def set_using_biased_012(self):
        self.fix_012 = False


class Strategy(object):
    def __init__(self):
        self.success = 0
        self.total = 0

    def set_fd(self, fd):
        self.fd = fd
        return fd

    # Select 0, 1, or 2
    def decision_first(self):
        return self.set_fd(0)

    # Switch or Not?
    def decision_second(self, opened_door):
        return True

    # Get the answer
    def result(self,success):
        if success:
            self.success += 1
        self.total += 1

    def stat(self):
        print("%s   \t%8d" % (self.__class__.__name__, self.success))

# Given the condition D0 != D1, return the other door.
def other_door(d0, d1):
    return 3 - (d0 + d1)

def choose_door_except(d0, rbg):
    return (d0 + rbg.random_number_0_or_1()) & 3

def final_door(fd, od, sw):
    if sw:
        return other_door(fd, od)
    else:
        return fd

def game(st, rbg_of_host, nullify_the_attack):
    car = rbg_of_host.random_number_012() # Do you like TOYOTA?
    first_choice = st.decision_first()
    if car == first_choice:
        od = choose_door_except(car, rbg_of_host)
    else:
        od = other_door(car, first_choice)
        if nullify_the_attack:
            rbg_of_host.random_number_0_or_1()
    sw = st.decision_second(od)
    decision = final_door(first_choice, od, sw)
    #
    if decision == car:
        success = True
    else:
        success = False
    st.result(success)


class Nobita(Strategy):
    def decision_first(self):
        return self.set_fd(randint(0,2))

    def decision_second(self, opened_door):
        return False

class Chuji(Nobita):
    def decision_second(self, opened_door):
        return choice([True,False])

class Marilyn(Nobita):
    def decision_second(self, opened_door):
        return True

class Tetsuya(Strategy):
    def decision_first(self):
        return self.set_fd(1)

    def decision_second(self, opened_door):
        return True

class Budai(Strategy):
    def decision_first(self):
        return self.set_fd(randint(0,1))

    def decision_second(self, opened_door):
        if opened_door == 1:
            return False
        else:
            return True

# gniibe observes if the host accesses his RBG to open the door.
# He likes TOYOTA, but he prefers goat better
class gniibe(Tetsuya):
    def __init__(self, rbg_of_host):
        Tetsuya.__init__(self)
        self.rbg = rbg_of_host

    def decision_first(self):
        self.observed_counter = self.rbg.examine()
        return Tetsuya.decision_first(self)

    def decision_second(self, opened_door):
        increment = self.observed_counter - self.rbg.examine()
        return not (increment == 0)


if __name__ == '__main__':
    import sys
    rbg_host = rbg()
    count = 1000000
    fix_game = False
    show_g = False
    while len(sys.argv) > 1:
        option = sys.argv[1]
        if option == '-n':      # -n for number of games
            sys.argv.pop(1)
            count = int(sys.argv[1])
            sys.argv.pop(1)
        elif option == '-b':    # -b for biased/broken RBG
            rbg_host.set_using_biased_012()
            sys.argv.pop(1)
        elif option == '-g':    # -g for including gniibe
            show_g = True
            sys.argv.pop(1)
        elif option == '-f':    # -f for fix the implementation of the game
            fix_game = True
            sys.argv.pop(1)
        else:
            break
    if len(sys.argv) > 1:
        print("\t\t\t\tRandom seed: \"%s\"" % sys.argv[1])
        seed(sys.argv[1])
        sys.argv.pop(1)
    else:
        seed()
    n = Nobita()
    c = Chuji()
    m = Marilyn()
    t = Tetsuya()
    b = Budai()
    g = gniibe(rbg_host)        # gniibe has access to the side channel
    for i in range(count):
        game(n, rbg_host, fix_game)
        game(c, rbg_host, fix_game)
        game(m, rbg_host, fix_game)
        game(t, rbg_host, fix_game)
        game(b, rbg_host, fix_game)
        game(g, rbg_host, fix_game)
    print("Player  \t Success")
    print("------------------------")
    n.stat()
    c.stat()
    m.stat()
    t.stat()
    b.stat()
    if show_g:
        g.stat()
