Task

Implement the classic children's game Rock-paper-scissors, as well as a simple predictive   AI   (artificial intelligence)   player.

Rock Paper Scissors is a two player game.

Each player chooses one of rock, paper or scissors, without knowing the other player's choice.

The winner is decided by a set of rules:

   Rock beats scissors
   Scissors beat paper
   Paper beats rock


If both players choose the same thing, there is no winner for that round.

For this task, the computer will be one of the players.

The operator will select Rock, Paper or Scissors and the computer will keep a record of the choice frequency, and use that information to make a weighted random choice in an attempt to defeat its opponent.



Extra credit

Support additional choices   additional weapons.




using System;
using System.Collections.Generic;
using System.Linq;
 
namespace RockPaperScissors
{
    class Program
    {
        static void Main(string[] args)
        {
            // There is no limit on the amount of weapons supported by RPSGame. Matchups are calculated depending on the order.
            var rps = new RPSGame("scissors", "paper", "rock", "lizard", "spock");
 
            int wins = 0, losses = 0, draws = 0;
 
            while (true)
            {
                Console.WriteLine("Make your move: " + string.Join(", ", rps.Weapons) + ", quit");
 
                string weapon = Console.ReadLine().Trim().ToLower();
 
                if (weapon == "quit")
                    break;
 
                if (!rps.Weapons.Contains(weapon))
                {
                    Console.WriteLine("Invalid weapon!");
                    continue;
                }
 
                int result = rps.Next(weapon);
 
                Console.WriteLine("You chose {0} and your opponent chose {1}!", weapon, rps.LastAIWeapon);
 
                switch (result)
                {
                    case 1: Console.WriteLine("{0} pwns {1}. You're a winner!", weapon, rps.LastAIWeapon);
                        wins++;
                        break;
                    case 0: Console.WriteLine("Draw!");
                        draws++;
                        break;
                    case -1: Console.WriteLine("{0} pwns {1}. You're a loser!", rps.LastAIWeapon, weapon);
                        losses++;
                        break;
                }
 
                Console.WriteLine();
            }
 
            Console.WriteLine("\nPlayer Statistics\nWins: {0}\nLosses: {1}\nDraws: {2}", wins, losses, draws);
        }
 
        class RPSGame
        {
            public RPSGame(params string[] weapons)
            {
                Weapons = weapons;
 
                // Creates a new AI opponent, and gives it the list of weapons.
                _rpsAI = new RPSAI(weapons);
            }
 
            // Play next turn.
            public int Next(string weapon)
            {
                string aiWeapon = _rpsAI.NextMove(); // Gets the AI opponent's next move.
                LastAIWeapon = aiWeapon; // Saves the AI opponent's move in a property so the player can see it.
 
                _rpsAI.AddPlayerMove(weapon); // Let the AI know which weapon the player chose, for future predictions.
                return GetWinner(Weapons, weapon, aiWeapon); // Returns -1 if AI win, 0 if draw, and 1 if player win.
            }
 
            // Returns matchup winner.
            public static int GetWinner(string[] weapons, string weapon1, string weapon2)
            {
                if (weapon1 == weapon2)
                    return 0; // If weapons are the same, return 0 for draw.
 
                if (GetVictories(weapons, weapon1).Contains(weapon2))
                    return 1; // Returns 1 for weapon1 win.
                else if (GetVictories(weapons, weapon2).Contains(weapon1))
                    return -1; // Returns -1 for weapon2 win.
 
                throw new Exception("No winner found.");
            }
 
            /* 
             * Return weapons that the provided weapon beats.
             * The are calculated in the following way:
             * If the index of the weapon is even, then all even indices less than it,
             * and all odd indices greater than it, are victories.
             * One exception is if it is an odd index, and also the last index in the set,
             * then the first index in the set is a victory.
             */
            public static IEnumerable<string> GetVictories(string[] weapons, string weapon)
            {
                // Gets index of weapon.
                int index = Array.IndexOf(weapons, weapon);
 
                // If weapon is odd and the final index in the set, then return the first item in the set as a victory.
                if (index % 2 != 0 && index == weapons.Length - 1)
                    yield return weapons[0];
 
                for (int i = index - 2; i >= 0; i -= 2)
                    yield return weapons[i];
 
                for (int i = index + 1; i < weapons.Length; i += 2)
                    yield return weapons[i];
            }
 
            public string LastAIWeapon
            {
                private set;
                get;
            }
 
            public readonly string[] Weapons;
            private RPSAI _rpsAI;
 
            class RPSAI
            {
                public RPSAI(params string[] weapons)
                {
                    _weapons = weapons;
                    _weaponProbability = new Dictionary<string, int>();
 
                    // The AI sets the probability for each weapon to be chosen as 1.
                    foreach (string weapon in weapons)
                        _weaponProbability.Add(weapon, 1);
 
                    _random = new Random();
                }
 
                // Increases probability of selecting each weapon that beats the provided move.
                public void AddPlayerMove(string weapon)
                {
                    int index = Array.IndexOf(_weapons, weapon);
 
                    foreach (string winWeapon in _weapons.Except(GetVictories(_weapons, weapon)))
                        if (winWeapon != weapon)
                            _weaponProbability[winWeapon]++;
                }
 
                // Gets the AI's next move.
                public string NextMove()
                {
                    double r = _random.NextDouble();
 
                    double divisor = _weaponProbability.Values.Sum();
 
                    var weightedWeaponRanges = new Dictionary<double, string>();
 
                    double currentPos = 0.0;
 
                    // Maps probabilities to ranges between 0.0 and 1.0. Returns weighted random weapon.
                    foreach (var weapon in _weaponProbability)
                    {
                        double weightedRange = weapon.Value / divisor;
                        if (r <= currentPos + (weapon.Value / divisor))
                            return weapon.Key;
                        currentPos += weightedRange;
                    }
 
                    throw new Exception("Error calculating move.");
                }
 
                Random _random;
                private readonly string[] _weapons;
                private Dictionary<string, int> _weaponProbability;
            }
        }
    }
}