Project Description
Like most of my other projects in this Rapid Game Development class, I started off this game simply trying to nail the fundamentals of a genre classic–in this case, the Concentration memory-matching game. From there, I tried to figure out an interesting spin (difficult, with Concentration’s simple ruleset and limited game pieces); what if you could match color OR number? What if you could only match even numbers of the same color, but odds of opposing suits? (One idea I didn’t consider at the time but would have been interesting — what if different colors and/or color/number combos gave you different scores?)
Ultimately, keeping the suit out of things and simply matching based on number was the way forward, and the “spin” on the game was to create an AI that wasn’t simply blindly guessing, but keeping track of the flipped cards on the board. As usual (see: DriftDodge & Dwarf Digger), adding a “charge bar” of sorts to the UI makes for a more engaging player experience. In this case, the red bar on the left represents the CPU’s knowledge level, while the blue bar on the right charges with each player miss, discharging when full and taking the CPU’s memory banks with it. In a more tuned game, the playing field would employ more cards so that the charge rate/maximum values would have more of an effect on the outcome; these numbers would lend themselves well to Easy/Medium/Hard difficulty settings, as well.
Programming Notes
- The card flipping animation is an adaptation of the code in this gist, which basically creates two sprite renderers (front & back) on a gameobject and swaps between them. You can’t directly assign two sprite renderers in the Inspector, so my implementation uses two children gameobjects for each card prefab; lerps their angles (depending on if we are flipping the card over or back to covered) and then swaps their sort order.
public IEnumerator FlipCard(bool uncover)
{
FaceUp = uncover;
if(FaceUp) gameManager.OnCardFaceUpStart(this);
float minAngle = uncover ? 0 : 180;
float maxAngle = uncover ? 180 : 0;
var t = 0f;
var uncovered = false;
while(t < 1f) {
t += Time.deltaTime * uncoverSpeed;;
var angle = Mathf.LerpAngle(minAngle, maxAngle, t);
// have to address the angles of the children directly
frontSprite.transform.eulerAngles = new Vector3(0, angle, 0);
backSprite.transform.eulerAngles = new Vector3(0, angle, 0);
if( angle is >= 90 and < 180 or >= 270 and < 360 && !uncovered) {
uncovered = true;
frontSprite.sortingOrder *= -1;
yield return null;
backSprite.sortingOrder *= -1;
}
yield return null;
}
if(FaceUp) gameManager.OnCardFaceUpEnd(this);
}
- The game manager keeps Lists of all cards in play and all cards the AI has seen face up. On the AI’s turn, they have a 25/75 chance of turning over a known card vs. a new, unknown card (favoring new cards to build their knowledge List):
// 75/25 whether we flip a new card or one we saw already
if(firstCardChance >= .25f || cardsEnemySawCount == 0)
{
Debug.Log("Starting with random out of " + cardsInPlayCount);
var unknownCards = CardsInPlay.Except(CardsEnemySaw).ToList();
if (unknownCards.Count == 0) unknownCards = CardsInPlay.ToList();
cardToFlip1 = unknownCards[Random.Range(0, unknownCards.Count)];
}
else
{
Debug.Log("Starting with known out of " + cardsEnemySawCount);
cardToFlip1 = CardsEnemySaw[Random.Range(0, cardsEnemySawCount)];
}
- From there, the AI has a 60/40 chance to recognize the match (in order to give the human player a fighting chance):
// enemy knows a match; 60/40 chance they recognize it
if ( secondCardChance >= .4f && CardsEnemySaw.Count(x => x.Number == cardToFlip1.Number) > 1 )
{
Debug.Log("Enemy Sees a Match for " + cardToFlip1.Suit + " " + cardToFlip1.Number);
var cardToFlip2 = CardsEnemySaw.First(x => x.Number == cardToFlip1.Number && x != cardToFlip1);
cardToFlip2.StartCoroutine(cardToFlip2.FlipCard(true));
}
else
{
Debug.Log("Enemy Flipping Random");
Card cardToFlip2;
var unknownCards = CardsInPlay.Except(CardsEnemySaw).ToList();
if (unknownCards.Count == 0) unknownCards = CardsInPlay.ToList();
do
{
cardToFlip2 = unknownCards[Random.Range(0, unknownCards.Count)];
} while (cardToFlip2==cardToFlip1); // make sure we aren't picking the same card twice & its new
cardToFlip2.StartCoroutine(cardToFlip2.FlipCard(true));
}