Artificial Intelligence in Game Opponents

I needed a CPU AI opponent for a simple game, so I decided to build one, and this article details my process. There’s no specific Unity content or project for this article – it’s all theory.

What is Game Artificial Intelligence?

Game artificial intelligence (AI) provides opponents for a human player. Those opponents need to simulate human (or robot, or animal) behaviour to provide a challenge.

A player wants opponents that act and react as a human opponent would. This is difficult because humans are organic, and computers are digital. And of course, computers don’t think for themselves like people can.

Game AI therefore needs to use purely logical algorithms to simulate the problem solving that a human player does organically. You need to make those algorithms feel natural.

A Simple Multiplayer Game

The game I’m creating AI for is a very simple card memory game, the kind where you have a grid of face-down cards, and you try to turn over two matching cards to win a point:

This game hardly requires a super-intelligent robot mind, but I found it was a great way to teach myself the fundamentals of game AI.

Define the Rules

To start with you need to know your game’s rules. Memory card games have very simple gameplay:

Players take turns to:

  • Turn over two cards, one at a time (making them face-up and visible to both players)
    • If the cards match, they are removed from play and the player gets a point and another turn
    • If the cards don’t match, they are turned back face down and the other player has their turn.

The player with the most points when all the cards are gone wins.

Implementing AI

I chose to implement a very rough AI to start with. AI version 1.0 picks cards completely at random. As you can guess, this is not challenging, and it doesn’t fit any definition of AI. But it did let me set up the programming infrastructure in my game.

My standard approach to adding functionality is to add in the skeleton first – typically the way the new functionality interacts with the rest of the code. Doing this lets me work on the new feature in isolation knowing that changes to it won’t affect the rest of my game (so long as I keep a simple interface between the main game and the new functionality).

I created a class called AiOpponent containing a single public method ‘ChooseCard()’. ChooseCard() returns a randomly selected card from the current game board. The core game knows that the player is AI and therefore must be asked to select a card rather than wait for a human player to tap the screen. The rest of the game continues as normal.

Once this was working I could encapsulate all my AI code into the AiOpponent class, but never have to change any other code in the game. The game manager doesn’t care how a card is selected, just that one is.

From Zero to Hero

I played the game a few times to figure out the main strategy. The core strategy is to remember the cards that are turned face-up and then returned face-down. The perfect strategy was to remember every card and then use that information to improve the chance of making pairs.

I decided that version 2.0 of my AI would play a ‘perfect game’ and remember every single card with 100% accuracy (this is actually simpler than implementing an imperfect strategy), and always make a pair when possible (i.e. when it knows where two matching cards are or when it knows where a card matching its first selection is). I broke this down into the following flowchart:

Considerations for v2.0

As you can probably guess, this AI is almost unbeatable. It doesn’t make mistakes, and it has a perfect memory. Unless you get lucky and also have a perfect memory you will usually lose. Like a random opponent, a perfect opponent isn’t any fun.

AI v2.0 doesn’t forget a card they saw several turns ago; it doesn’t accidentally pick up the card next to the one it intended. AI v2.0 is too machine-like; too perfect.

AI Overkill

I’m the first to admit that the AI for my card memory game is overkill. It’s a simple kids’ game, and having a purely random opponent would work just fine. But I wanted to teach myself how to create an AI opponent so I can apply that knowledge to a more complex game I plan to make later.

So I set out to make the most human card memory AI I could.

v3.0

The next step was to make the AI adaptable to different difficulty levels by blending the random card selection from v1.0 and the perfect matching skills of v2.0. Based on the difficulty level my AI would choose randomly or perfectly in different proportions.

  • Easy AI – totally random
  • Medium AI – half random, half perfect
  • Hard AI – totally perfect.

v3.0 is probably the most complex AI you could possibly need for this kind of game. In fact, it’s probably already overkill. But once I’d started deconstructing the game I realised even this simple game has more potential for AI, so in a future post I will go into the nuances of a more human card memory opponent (even though it’s overkill).

2 thoughts on “Artificial Intelligence in Game Opponents

  1. Hello,
    I might be a little late but, would you care sharing your AI script code? I’m planning doing one for the exact same use and I was wondering if I could do mine inspired by yours.
    Thank you!

    • I’m not sure how useful this code will be, as it is old and ripped right out of a project. It should at the very least give you a basic idea of how it works. I haven’t seen this code myself in ~5 years, so I was surprised that it actually has quite a lot of comments explaining how it works. The comments are probably more useful than the code:

      using System;
      using System.Collections.Generic;
      using System.Xml.Serialization;


      namespace Match
      {
      public class AIOpponent
      {
      // this stores the logic for the AI opponent
      public DataTypes.MatchPlayer AiPlayer;

      public AIOpponent()
      {
      }
      public AIOpponent(int skillLevel)
      {
      AiPlayer = new DataTypes.MatchPlayer(new DataTypes.Player());
      AiPlayer.IsAiOpponent = true;

      rememberedCards = new List();
      GameSettings.DifficultyLevel = skillLevel;
      switch (GameSettings.DifficultyLevel)
      {
      case 1:
      AiPlayer.Username = "Easy CPU";
      AiPlayer.AvatarImage = "ms-appx:///Images/Icons/player_1.png";
      break;
      case 5:
      AiPlayer.Username = "Medium CPU";
      AiPlayer.AvatarImage = "ms-appx:///Images/Icons/player_2.png";
      break;
      case 8:
      AiPlayer.Username = "Hard CPU";
      AiPlayer.AvatarImage = "ms-appx:///Images/Icons/player_3.png";
      break;
      case 10:
      AiPlayer.Username = "Impossible CPU";
      AiPlayer.AvatarImage = "ms-appx:///Images/Icons/player_4.png";
      break;
      default:
      AiPlayer.Username = "Error with AI";
      AiPlayer.AvatarImage = "ms-appx:///Images/Icons/player_1.png";
      break;

      }

      }

      [XmlIgnore]
      private DataTypes.Card firstCard { get; set; } // should not need to save this as the game should not be saved during a turn

      private bool RandomlyOverrideSkill()
      {
      // pick a number between 1 and the difficulty level
      // if number == difficulty level then play randomly instead of using skill
      // this makes the AI opponent play more randomly the lower the difficulty level is
      var rand = new Random();
      int s = rand.Next(1, GameSettings.DifficultyLevel + 1);

      if (s == GameSettings.DifficultyLevel)
      {
      // pick randomly
      return true;
      }
      else
      {
      // pick skillfully
      return false;
      }
      }

      public DataTypes.Card SelectedCard(List cardsList, bool isFirstCard)
      {
      if (isFirstCard)
      {
      firstCard = FindPairedOrRandomCard(cardsList);
      return firstCard;
      }
      else
      {
      return FindMatchingOrRandomCard(cardsList, firstCard);
      }
      }

      [XmlArrayItem(typeof(Clown)),
      XmlArrayItem(typeof(HoseClown)),
      XmlArrayItem(typeof(Cannon)),
      XmlArrayItem(typeof(LargeBall)),
      XmlArrayItem(typeof(MediumBall)),
      XmlArrayItem(typeof(ClownCar)),
      XmlArrayItem(typeof(ClownShoe)),
      XmlArrayItem(typeof(Balloon))]
      public List rememberedCards { get; set; }

      public void ResetAI()
      {
      rememberedCards = new List();
      }

      // holds a list of cards the AI has seen/remembered
      // the list sets the index value to true for all cards seen
      // the selection algorithms decide if the AI accurately acts on the information (e.g. making it forget some cards or picking an adjacent card by 'mistake')
      //static List rememberedCards;

      public void RememberCard(DataTypes.Card card, List cardsList)
      {

      // the AI remembers cards it has seen face up
      // based on difficulty level the AI will occasionally not remember cards it has seen
      // higher the difficulty the more chance of remembering the card
      var rand = new Random();

      // card is easier to remember if it is on a corner or edge
      CardPosition position = GetCardPos(cardsList.IndexOf(card));
      // temporarily increase the difficulty based on card position
      int modifiedDifficulty = GameSettings.DifficultyLevel + 1;

      // AI memory is improved when the card is at the corner or edge
      if(position == CardPosition.Corner)
      {
      modifiedDifficulty += 5;
      }
      else if(position == CardPosition.Edge)
      {
      modifiedDifficulty += 2;
      }

      int s = rand.Next(0, modifiedDifficulty);

      if (s != 1)
      {
      // rememberedCards[index] = true;
      if (!rememberedCards.Contains(card))
      {
      rememberedCards.Add(card);
      }
      }
      }

      private CardPosition GetCardPos(int p)
      {
      switch(p)
      {
      case 0:
      case 3:
      case 12:
      case 15:
      return CardPosition.Corner;
      case 1:
      case 2:
      case 4:
      case 7:
      case 8:
      case 11:
      case 13:
      case 14:
      return CardPosition.Edge;
      default:
      return CardPosition.Middle;

      }
      }

      enum CardPosition
      {
      Middle,
      Edge,
      Corner

      }

      private DataTypes.Card PickRandomCard(List cardsList)
      {
      // used to pick a card completely at random from the live and face-down cards

      Random rand = new Random();

      retry:

      int randNum = rand.Next(0, cardsList.Count);
      if (cardsList[randNum].IsLive && cardsList[randNum].IsFaceDown)
      {
      return cardsList[randNum];
      }
      else
      {
      goto retry;
      }
      }

      private DataTypes.Card FindPairedOrRandomCard(List cardsList)
      {
      //List foundPair = new List();

      //TODO: this needs to be more random/more varied. Currently will find a pair with the first card in the list that has a known pair.
      // should be weighted somehow
      if (RandomlyOverrideSkill())
      {
      return PickRandomCard(cardsList);
      }

      foreach (DataTypes.Card card in rememberedCards)
      {
      if (card.IsLive)
      {
      foreach (DataTypes.Card otherCard in rememberedCards)
      {
      if (otherCard != card) // don't compare card to itself
      {
      if (otherCard.CardType == card.CardType && otherCard.IsLive)
      {
      //foundPair.Add(card);
      //foundPair.Add(otherCard);
      //return foundPair;
      return card;
      }
      }
      }
      }
      }

      // if no pair found, return a random card
      return PickRandomCard(cardsList);
      }

      private DataTypes.Card FindMatchingOrRandomCard(List cardsList, DataTypes.Card card)
      {
      // tries to find a card matching the one given in the args
      // otherwise returns random card
      // used to find a second card matching an already selected card
      // override with a random selection based on difficulty level
      if (RandomlyOverrideSkill())
      {
      return PickRandomCard(cardsList);
      }
      foreach (DataTypes.Card otherCard in rememberedCards)
      {
      if (otherCard != card && otherCard.CardType == card.CardType && otherCard.IsLive)
      {
      //foundPair.Add(card);
      //foundPair.Add(otherCard);
      //return foundPair;
      return otherCard;
      }
      }
      return PickRandomCard(cardsList);
      }
      }
      }

Leave a Reply to Damien Allan Cancel reply