# rock_paper_scissors
#
# script designed to win repeated games of rock/paper/scissors by
# using probability lookup techniques.

from __future__ import division
import cPickle
import random

class RockPaperScissors:

    # just set up the defaults
    def __init__(self):
        self.moves = ["" for x in range(3)]
        self.possible_moves = ["rock", "paper", "scissors"]
        self.possible_sequences = self._possible_sequences()
        self.frequencies = self._frequencies()
        self.prediction_matrix = self._prediction_matrix()

    # load data from pickle format
    def load(self):
        database = open("rps2_data.txt", "r")
        try:
            # loading the data throws an EOFError the first time the script
            # runs, since the data won't have been written yet
            self.frequencies = cPickle.load(database)
            self.prediction_matrix = cPickle.load(database)
        except EOFError:
            pass
        database.close()

    # save data to pickle format
    def store(self):
        database = open("rps2_data.txt", "w")
        cPickle.dump(self.frequencies, database)
        cPickle.dump(self.prediction_matrix, database)
        database.close()

    # main method; play the game
    def play(self):
        self.load()

        user_move = ""
        self.moves[0] = self.user_move(user_move)
        self.moves[1] = self.user_move(user_move)
        prediction = self.predict(self.moves)
        self.moves[2] = self.user_move(user_move)

        print "predicted third move " + prediction
        if prediction == self.moves[2]:
            print "rawk!"

        self.learn(self.moves)
        self.store()

    # acquire moves interactively in Windows
    def user_move(self, user_move):
        while user_move != "rock" and user_move != "paper" and user_move != "scissors":
		user_move = raw_input("rock/paper/scissors:   ")
        return user_move

    # internal only; list possible sequences.
    def _possible_sequences(self):
        # the data structure here is a list of tuples. por ejemplo:
        #     "rock_paper", "scissors"
        #     "rock_paper", "rock"
        # ...for every permutation of the rock/paper/scissors set, which obviously has 27
        # possible versions, given that it is a 3x3x3 matrix. possible_sequences is basically
        # the data structure which represents all possible permutations. a more sophisticated
        # version would use a three-d data structure and model probabilities for the second
        # given the first, but that's not planned at the moment. this is based on the kung fu 
        # fighting moves C++ prediction code in O'Reilly's "AI for Game Developers" Bayesian
        # networks chapter, which is really pretty simplistic. the network has only three nodes,
        # here representing the three moves in a rock-paper-scissors game, and the first two
        # moves simply feed the frequency matrices for the third node's probability calculations.
        # really the network only has two nodes, since the second blocks the first.
        _ps = []
        possible_seq = ()
        for x in self.possible_moves:
             for y in self.possible_moves:
                 for z in self.possible_moves:
                     possible_seq = x + "_" + y, z
                     _ps.append(possible_seq)
        return _ps

    # internal only; define default frequencies
    def _frequencies(self):
        # these are the frequencies for each two-move sequence. ie:
        #     {"rock_paper": 23}
        # ...which would mean that the first move had been rock and the second move had been
        # paper 24 distinct times so far. this data is needed for the probability calculations.
        _frqs = {}
        for x in self.possible_sequences:
            _frqs[x[0]] = 0
        return _frqs

    # internal only; default (empty) matrix
    def _prediction_matrix(self):
        # this records the frequency with which initial two-move sequences are followed by a
        # given third move. ie:
        #     {("rock_paper", "scissors"): 23}
        # which would mean that 24 times, when rock was followed by paper, scissors was the
        # third move. this data also needed for probability calculations.
        _cfm = {}
        for x in self.possible_sequences:
            _cfm[x] = 0
        return _cfm

    # populate frequency table/prediction matrix
    def learn(self, moves):
        # all we do here is increment the frequency numbers stored in the frequencies dict and
        # the prediction_matrix dict (which is indexed by a tuple). you'll need to check out
        # the methods which create those data structures to make sense of them, but it's pretty
        # straightforward.
        index = moves[0] + "_" + moves[1]
        self.frequencies[index] = self.frequencies[index] + 1
        self.prediction_matrix[index, moves[2]] = self.prediction_matrix[index, moves[2]] + 1
        
    # based on first two moves, predict third
    def predict(self, moves):
        # the probability of any given move comes from an equation; divide the frequency with
        # which a two-move sequence is followed by a given move by the overall frequency of
        # that two-move sequence. if either frequency is equal to zero, the probability table
        # is still in its infancy, and we resort to a random guess. this method is the reason
        # the script imports both random (for the guessing) and __future__.division (to ensure
        # floating-point probability values).
        two_move_seq = moves[0] + "_" + moves[1]
        overall_frequency = self.frequencies[two_move_seq]
        most_probable_move = ()
        move_probability = 0
        seq_followed_by_move = 0

        for move in self.possible_moves:
            seq_followed_by_move = self.prediction_matrix[(two_move_seq, move)]
            if seq_followed_by_move != 0 and overall_frequency != 0:
                move_probability = seq_followed_by_move / overall_frequency
            else:
                move_probability = random.random()
            if not most_probable_move or most_probable_move[1] < move_probability:
                # the highest move probability replaces any other, even if the probability is
                # randomly generated; this favors populating the prediction matrix at the
                # expense of certainty.
                most_probable_move = (move, move_probability)

        return most_probable_move[0]