Click here to Skip to main content
15,867,308 members
Articles / Artificial Intelligence

The Game of Clue (C# 2010)

Rate me:
Please Sign up or sign in to vote.
4.93/5 (55 votes)
13 Aug 2010CPOL29 min read 131.8K   3K   102   47
You are a suspect!

The Game of Clue

According to Idea Finder, the game of ClueDo was invented by Anthony Ernest Pratt and patented in England in 1947, then purchased by Waddington Games before the U.S. rights were bought by Parker Brothers in 1949. The WhoDunnit? game has been a mainstay of rainy days and cottage cupboards around the world ever since. There have been several PC versions of this murder mystery, and after passing up the last copy I'd seen on a store shelf and then failing to find another one again, I went to the nearest thrift shop and bought a used copy of the board game for $2, and started on this project about a month ago, and it looks pretty cool.

Playing the Game

If all you want to do is play your old favourite and you already have Microsoft's C#2010 up and running, then all you need to do is download the source code above and let yourself into Mr. Boddy's Mansion on the day of the murder. You can play any suspect you like by making your selection when prompted.

Choose_player.PNG

It's a one-player game, so the other players are all controlled by the computer's Artificial Intelligence, which you can set via the menu Options->Set-AI.

ai-controls.PNG

Once you have this menu up, you can cycle through the various AI-levels by clicking on the character's name. It would be nice if you could do the same thing with your own 'intelligence', but even if you're playing Mustard and you set Mustard's AI to genius, you'll still have to do all the work yourself.

There's also something called Auto-Proceed that's part of the speed controls, which you can access through the menu Options->Speed.

speed_control.PNG

In the default setting, Auto-Proceed will advance the game in a way similar to when you're playing with people around a table so that it is difficult to keep up and take careful notes. You may want to switch this off the first time you play, and manually click the 'OK' button to move the game along while you do your sleuthing. Which brings us to the Notebook on the right of the game board.

Keeping Notes

notebook.PNG

In the image above, you'll notice a few things. First, the names on the left are the names of the game cards sorted out into either Suspect-Cards, Weapon-Cards, or Room-Cards. Take a look at the left edge of the image, and you'll see the three cards your suspect is holding; in this particular example: Revolver, Conservatory, and Billiard Room. To the right of the names, you can see six colored boxes. These boxes are user-controlled. You can either right-click or left-click them to cycle through the four possible values each colored box can hold: X, check, ?, or blank. There's a color for each suspect, and your own suspect's boxes are automatically set at the beginning of the game to help you along. Xs mean that suspect definitely does not have that card, and a check means he does, while a blank means you have no idea either way, and a ? means you think he might. This should be plenty to help you solve the puzzle.

Let's look at the game board.

game_board.PNG

You can clearly see the 9 rooms and the marble tiles. In the image above, the two characters Miss Scarlet and Col. Mustard have played their opening turns and stepped away from their respective start locations towards the rooms. The object of the game is to travel from room to room making Suggestions and getting other players (suspects like yourself) to show you their cards. When you've figured out what cards they all have, you should know what three cards have been separated from the rest and now rest in the middle of the board, and win the game.

The control-panel is on the right on your notebook.

control_board.PNG

and consists of a few Pulse-Buttons for you to either Roll the die, Make a Suggestion, or End your turn, which are all self-explanatory. Below these buttons, you have the die-box where the die (that's the square thing with spots on it!) is rolled. And, beneath the die-box is your ultimate button Accuse, which is all you need to make someone's life a real bother.

Making a Suggestion

At the beginning of your turn, you'll either get a chance to suggest right away (if some other player summoned you into a new room to make their suggestion), or you'll have to move out of a room. In either case, whenever the Suggestion button is enabled, you're entitled to make a new suggestion, and that'll happen every time you enter a room. It's probably a good idea to make suggestions every chance you get.

making_a_suggestion.PNG

Once you've clicked the Suggest button, your game board will look like the image above. You must suggest the room you're in when making a suggestion, so your only options are the Who and the How about it. To make your choice, you simply click on the face of the suspect and the image of the weapon you want to suggest, and when you're done deciding, the images will rotate into position; click the OK button that appears on the bottom of the 'Bubble' whenever you're ready.

At this point, the Suggestion needs to be Proven. So the player on your left (or at least that's where he'd be if you were sitting around the table) will have to try to disprove your suggestion by showing you any of the cards you're asking for. If they don't have a card, they'll tell you so:

proving_a_suggestion___no_card.PNG

If she does have a card, she'll show you what she has and that'll be the end of your suggestion, but whenever another player is making a suggestion, they and the person disproving their suggestion are the only people around the table who know what card is being shown. All the other players have to try and figure out what card it is by using deduction and reason.

proving_a_suggestion___unknown_card.PNG

It will happen several times during the game that someone else will make a suggestion and it will be your turn to try to disprove it. When you cannot disprove a suggestion, a banner will appear telling you "you have no card", or if you can, the same banner will say that you do have a card and it will show you what cards you can show. Here, if the auto-proceed setting is checked, the game will go on without your interfering if you have none or only one card, but even if you have not shut off the auto-proceed option, the game will stop and wait for you to pick which card you want to show the player making the suggestion when you have more than one of the cards being suggested.

proving_a_suggestion___you_can_show_a_card.PNG

Secret Passages

Before you roll the die, first consider whether you want to use a Secret Passage. The secret passages are located in the corner rooms, and let you travel across the board without rolling the die. So, if you're in a room and you want to go to the opposite corner, you'll then want to use the secret passage. To do so, you click on to the colored square that appears in the room that you're in before rolling the die. The image below shows the game-board at the beginning of Col. Mustard's turn, and he has not yet decided whether he wants to roll the die or use the secret passage. You can still see the the yellow square in the corner indicating that Col. Mustard has the option of using the secret passage and go to the Conservatory from the Lounge rather than roll the die.

using_the_secret_passage.PNG

Let's Roll!

To roll the die, you click the Roll button. When the roll-die animation stops, the game board will display where you can move by placing colored squares on all marble tiles and doors where you can go.

move_options.PNG

The image above shows the move options for Col. Mustard when he rolls a 6 at the start of the game. Just click on the game board, and Col. Mustard will move to wherever you told him to.

I Accuse!

When you've suggested enough and have it all figured out, go ahead and make your accusation and close the game, but be careful... if you accuse incorrectly you'll eliminate yourself and have to watch the AI until some other sleuth solves Mr. Boddy's mysterious murder. To accuse, you only need to click the Accuse button below the die-box on the right.

accuse.PNG

Things are a little different here in that you can accuse anyone anywhere with any weapon. So you don't actually have to be in any specific room, which means you can select whatever room you think is the crime-scene. The image above shows you the accuse options, and you can make your pick by clicking the appropriate images like you did with your suggestions. To cancel your accusation, click the Accuse button again when it reads Cancel Accusation, and you can wuss-out and let someone else be the hero. Or, if you've got the juice, click that big red pulsing button in the middle of the screen and see if you're an Ace or a Waste!

The Code

The Marblation Process

This project was a lot of fun to write, and it went pretty well all the way through, but the first problem was the board itself. All of the images that appear in this game were downloaded off the internet (the only thing I actually drew myself was the piece of cheese!). But there weren't any pictures of the board itself that I could use for the game since the graphics animation required each tile to be perfectly aligned (if I wanted to do things the easy way) to facilitate the placing of the images of the suspects in their proper places. For this reason, I had to draw the game board myself... So, OK, the game board and the piece of cheese. This was actually harder than you'd think. For the marble tiles, I painted all the blank tiles yellow, and then whited-out a few disparate tiles at a time while cutting and pasting the entire transparent map (white tiles) onto a mega image of a marble slab, each time whiting out a few new tiles and picking up marble when I pasted the latest image until all the tiles were marbled. Whiting them all out and pasting the board onto a marble slab wouldn't look so cool, because then the whole floor would have had a single marble image behind it. This is way better.

Moving the Pieces

To keep track of which tiles the suspects can move on as well as the dimensions of each room, I put together a miniature map using MS-Paint depicting the game's floor, where each pixel is equivalent to a single tile. Here's the image:

floor_tiles.PNG

There's probably a better way to do this, but doing it this way allowed me to visualize the game's data better than if I had written it directly into code in a database format. The white pixels are marble tiles on the game board, the red-tiles are rooms, black tiles are inaccessible, and the blue ones are doors. The difficult thing to do here was 'program' the door-pixels in the image to contain information like RoomID and dirDoor. To do this:

blue is not quite so blue, but neither should you
                 a poem by Christ Kennedy

(I've applied to the CodeProject to become their on-line poet-laureate, but haven't yet heard back from them; a petition from all my fans would greatly improve the chances that this site finally has the proper poet it truly needs.)

The blue pixels on the mini-map do hold this information. The color blue in the blue pixels, like all pixels, is defined by four byte-sized variables: R (red), G (green), B (blue), and A (Alpha). Though the blue doors all have the same 255 for the B component, the R and G values hold the egress direction and room ID values, respectively. You can see this in the classSuspect's function shown below:

C#
/// <summary>
/// uses the 24x24 pixel bitmap bmpClueFloor to set up 2d array describing the game map.
/// each pixel represents a square on the board.
/// the pixel color components (Red, Green, Blue) determines the type of tile.
/// Marble Tile (corridors) : R=255, 
///                           G=255, 
///                           B=255                          (white).
/// Illegal square          : R=0, 
///                           G=0, 
///                           B=0                            (black).
/// Room                    : R=255, 
///                           G = ROOM ID NUMBER (study =0, 
///                               hall =1, ... billiard room = 7, library =8), 
///                           B=0                            (variant of Red).
/// Door                    : R=egress direction (0=north, 1=east, 
//                                     2=south, 3=west, 4=secretpassage), 
///                           G=Room ID, 
///                           B=255                          (variant of Blue).
/// e.g. the marble tile south of the door
///      (6,4 in 2d array) to the study is white (0,0,0).
///      the first tile into the study(roomId=0) is considered a door-tile 
///      facing south(dir=2) therefore tile (6,3 in 2d array) is colored (2,0,255).
///      the top-left-most tile (0,0) is the study's secret passage
///      to the kitchen(roomID=4) therefore its tile is colored(4,4,255).
///      the right-bottom-most tile (23,23) is the kitchen's secret passage
///      to the Study(roomID=0) therefore its tile is colored (4,0,255).
/// </summary>
void initFloor()
{ // static floor tiles 
    Bitmap bmpFloorTiles = new Bitmap(Clue_CS2010.Properties.Resources.ClueFloor);
    cFloor = new classFloor[bmpFloorTiles.Width, bmpFloorTiles.Height];
    for (int intX = 0; intX < bmpFloorTiles.Width; intX++)
        for (int intY = 0; intY < bmpFloorTiles.Height; intY++)
        {
            //if (intX == 0 && intY == 23)
            //    MessageBox.Show("stop initFloor()");
            cFloor[intX, intY] = new classFloor();
            Color clrTile = bmpFloorTiles.GetPixel(intX, intY);
                if (clrTile.R == 255 && clrTile.G == 255 && clrTile.B == 255)
            { // white = tile
                cFloor[intX, intY].eType = enuFloor.tile;
            }
            else if (clrTile.R == 0 && clrTile.G == 0 && clrTile.B == 0)
            { // black = invalid
                cFloor[intX, intY].eType = enuFloor.invalid;
            }
            else if (clrTile.B == 255)
            { // blue = door : R = direction door exits room, G = roomID#
                cFloor[intX, intY].eType = enuFloor.door;
                cFloor[intX, intY].dirDoor = (enuDir)clrTile.R;
                cFloor[intX, intY].eRoom = (enuRooms)clrTile.G;
            }
            else if (clrTile.R == 255)
            { // red = room : G = roomID#
                cFloor[intX, intY].eType = enuFloor.room;
                cFloor[intX, intY].eRoom = (enuRooms)clrTile.G;
            }
            else
                MessageBox.Show("this should not happen");
        }
}

There's also color-code for the rooms (red pixels), but that turned out to be unnecessary since the suspects only travel along tiles and then are inside a room, and when they're inside a room, they are drawn at their assigned locations around a central point so the red tiles may as well be black (though that would make it harder to visualize the map). In any case, once this cFloor array is set and we have our floor set up in a data format which the code can easily read, the rest of it is quite simple.

Well, actually, there were a few other problems.

Both the player-controlled suspects and the AI-controlled suspects use much the same code, and the same thing happens when it's an AI player's turn to move and it decides to roll the die, or when it's the human player who presses the roll-die button: the die is cast! And when that animation stops, the program has to decide what squares that particular suspect is allowed to go to. To do this, it uses an algorithm called Breadth First Search, which I describe in an earlier article: Battlefield Simulator. Essentially, it moves step by step away from the suspect's current location, keeping track of where it can go at each round of stepping, while stepping one step further each round, until it reaches the limit (die roll result), all the while keeping track of where the suspect can go in an array called:

C#
public classSearchElement[,] cSEAllowableMoves;

of the class shown below:

C#
public class classSearchElement
{
    public Point pt;
    public int intSteps;
    public int intTurns;
    public int intCost;

    public enuDir SrcDir;
    public classSearchElement next;

    public void set(Point PT, int Cost, enuDir sourceDirection)
    { set(PT, sourceDirection, Cost, 0); }

    public void set(Point PT, enuDir sourceDirection, int Steps, int turns)
    {
        pt = PT;
        SrcDir = sourceDirection;
        intSteps = Steps;
        intTurns = turns;
        intCost = intTurns * 3 + intSteps;
    }
}

Since the array is initialized to contain nothing but null values (note that all the previous elements are inserted into a linked list so that they can be recycled the next time a search is made), when the player (or AI-suspect) clicks on the screen, the pictureBox click-event handler tests if the tile corresponding to the area of the screen that was clicked is null; if it is, then that click is ignored; if it isn't, then it knows that that tile is a valid place for that suspect to move to.

Then, when it knows where the player wants to go, it does another similar breadth first search in the classSuspect function shortestPath() using the same classSearchElement shown above and stepping from the destination to the current location, while keeping track of the path along which it travels (via the SrcDir variable in classSearchElement). Then the animation begins.

Mode à la mode

One of the most important variables in this project is eMode of type enuMode, shown below:

C#
public enum enuMode 
{
    idle,
    dealCards,
    Accuse,
    Accuse_Animation,
    chooseCharacter,
    warnPlayerNextTurn,
    beginTurn,
    gameOver,
    rollDie_begin,
    rollDie_animate,
    rollDie_end,
    showAllowableMoves,
    animateMove,
    Suggest,
    AISuggestionComplete,
    animateIntroduction,
    animateSummonSuspect,
    animateSummonWeapon,
    animateSecretPassage,
    respondToSuggestion,
    playerChoosesCardToShow,
    playerTurnIdle,
    endTurn,
    any
};

Whenever tmrDelay is enabled and ticks, its event handler checks to see what mode the game is in before deciding what needs to be done next. When the suspect is moving from one tile to the next, or entering or leaving a room by a door, then the eMode variable is set to animateMove and the flow of the program falls into the function moveStep().

The moveStep() function moves the suspect's icon over the board along the path which the shortestPath() function calculated and stored in the global udrMoveAnimationInfo structure variable, moving the piece along by steps of 7 pixels (each tile is 28 x 28 pixels in area) in whatever direction the icon is moving from one tile to the next, until it reaches its destination. classSuspect's location is described with two variables: eRoom and ptTile. Since the eRoom variable tells the program that that suspect is either in a room (specifies which room), at start-position, or on a tile, the only time the ptTile variable is needed is when the suspect's eRoom value is set to tiles.

Artificial Intelligence

Once I had made myself a six-player game of clue (with many details left undone), I set out to make the smartest, perfectest, geniusistic artificial intelligence I could conjure. The simplest way to simulate this would be to count the number of turns each suspect plays, and then decide that after so many turns, there's a chance that this or that suspect might 'guess' the solution and just have it look at the cards. In other words, cheat.

But what would be the fun in that!?!

Nope, that's not what this does at all. The genius setting takes advantage of every trick I've given it. And though I am no genius, the AI is pretty close to it. To geniusify my computer, I had it take careful notes of what cards each suspect declares they do not have, as well as each set of cards that were called when they did show a card. This way, at every cycle of the Prove Suggestion phase of the game, it can check if previous sets of unknown cards included a card any suspect recently declared not having, until it eliminates two of those three and then knows exactly what card was shown any number of turns ago, even though they didn't see it themselves.

For example:

Mrs.White suggests :

Col. Mustard with Knife in the Kitchen.

and Mrs. Peacock says she has no card.

But previously, Miss Scarlet suggested

Col. Mustard with the rope in the kitchen.

and Mrs. Peacock, that turn, did show Miss Scarlet a card. A mystery card which we can now deduce was in fact the Rope because we now know that Mrs. Peacock does not have Col. Mustard and Mrs. Peacock does not have the Kitchen, she must therefore have shown Miss Scarlet the Rope.

Follow?

The genius AI remembers all sets of unknown cards, and keeps diligent notes of who denied having what, making it fairly easy for it to figure out relatively quickly what the solution to the puzzle is. It also knows how many cards each suspect holds in their hand (as of now, in a six player game, each suspect holds three cards each), and so once those three cards have been identified, the genius-AI knows that that suspect does not have any other cards. To solve the puzzle, it can eliminate each possibility off of a list, or it can notice that no suspects have this or that card, making that one card the obvious choice. Whenever its list of suspects, list of weapons, and list of rooms is narrowed down to one of each, then it makes its accusation. And, the computer is never wrong (I didn't bother making the dumber levels erroneous, just less diligent).

But keeping good notes isn't the be-all and end-all of winning a good game of Clue. You still have to ask the right questions. classSuspect does this in the function below:

C#
public void makeSuggestion()
{
    prepareSuggestionAI();

    pickSuspectForSuggestion();
    pickWeaponForSuggestion();
    MainForm.udrSuggestion.eRoom = eRoom;
 ...

by first picking a suspect, then a weapon. To make the selection of the suspect, it goes over its notes, trying to eliminate 'unknowns', those are the sets of cards which other suspects have shown to a third party. If it knows the Who in this whodunit?, then it may either choose that card, and be certain it doesn't relearn what it already knows, or it may choose a card it holds in its hand (if a computer actually had hands, I guess). But before it can figure out who killed Mr. Boddy, it has to eliminate the possibilities by looking over the 'unknowns'. If it has no mystery unknown for the suspect category, it just randomly picks any suspects remaining on its list. But if it does have one or more of these unknown suspect cards left to eliminate, it will go around the table in the reverse order in which the suggestion will be disproved, and then pick the first suspect-unknown-card it finds. It goes in the reverse order because the notion is that it's best to collect as many Xs on your notebook as possible. If the first suspect you ask to disprove your suggestion shows you a card, you've narrowed one field (suspect, weapon, or room) by one card, but you haven't learned much else. By collecting all these Xs on your notebook, it becomes much easier to deduce what cards are being shown to any third party, and the puzzle is solved much more quickly.

Then the AI picks a weapon to suggest by essentially doing the same thing it did to pick a suspect, but keeping in mind that it doesn't want to ask about a mystery unknown weapon-card which is in the same suspect-column as the mystery-unknown suspect card it has already picked. For example, if it knows Prof. Plum showed a card when he was asked for the Kitchen, Peacock, and Candlestick, and it has already decided to suggest Peacock, then it does not want to suggest Candlestick because if it eliminates one, it's already resolved the other. It can learn more by asking about a different weapon, a weapon which it does not suspect the same suspect has (forgive the confusion). Collecting more Xs, resolving more unknowns, and reducing more possibilities, until a clear deduction of the who and the where and the how is made to end the game. See? Simple.

Quick and neat.

The AI's internal note keeping makes regular use of two important functions:

C#
void checkNote(enuCards eCard, enuSuspects eSuspect)

and

C#
public void XNote(enuCards eCard, enuSuspects eSuspect)

Notes here refer to individual cells that represent internally what the user sees graphically on the notebook, the colored squares. There are 6x21 notes in a 2-dimensional array for each suspect (6) and each card (21). The AI checks a note whenever it has deduced or has been shown that a given suspect has a card.

checkNote()

The checkNote(eSuspect, eCard) function sets a specific note in the array to 'check', meaning that the holder of the card eCard has been identified as the suspect eSuspect, and if the holder of a card has been identified, then all the other suspects are known to not have the same card (there is only one of each card), and so all the other suspects have their note for that particular card X'ed (using Xnote() below). Also, when the holder of a new card has been identified, then a tally of cards which that suspect is known to have is made, and if all of that suspect's cards have been identified, then it knows that that suspect has no other cards, so all other cards are X'ed.

XNote()

X'ing a card by using the XNote(eSuspect, eCard) function is similar to checkNote() above, except that it lets the AI make a record of the knowledge that the suspect eSuspect does not have the card eCard. When this happens, it goes over all of its sets of mystery unknown cards and looks for one that includes the card eCard which that suspect has recently been discovered not to have, and eliminates these unknowns. By eliminating unknowns, it can reduce the number of cards in unknown card-sets to one and can then check (using checkNote() above) that card in its notes. If all suspects are known not to have a given card, then the AI knows that that card is part of the solution and adjusts its suggestion making accordingly.

Between X'ing and Check'ing, both Check'ing and X'ing, the X'ing and Check'ing soon cascades into a lot of figuring, and in no time, soon the whole muddled mystery unravels nicely.

Dummifying the Geniustic AI

Lobotomy! Yes, you guessed it. If all suspects around the table were geniuses, you would soon get frustrated trying to play this game. It takes luck (and untoggling of the Auto-Proceed helps!) in order to win a game if you're playing against a bunch of geniuses. First of all, the geniuses remember everything, never make mistakes, and are very diligent in their note-taking about everything that happens in the game. You can still tweak your suggestion making and get to the answer pretty quick because there are ways to beat it, but ... most people just like to play against us regular folk. So that means the AI needs to be a bit stupid sometimes. Luckily that's not much of a bother. There are five levels of AI, ranging from Stupid-to-Genius. And to dummificate a geniustic computer, all it takes is a nasty virus, or a few lines of code that involve the use of something called a Random Number Generator.

Here's an example where the AI decides whether it wants to count the number of identified cards for this suspect and then X'ify the remaining cards if it has identified all of the cards which that suspect holds:

C#
// ai may notice that all of this suspect's cards have been identified
if ((eAILevel == enuAILevel.stupid && (int)(MainForm.rnd.NextDouble() * 1000) % 400 == 0)  
 || (eAILevel == enuAILevel.dull && (int)(MainForm.rnd.NextDouble() * 1000) % 50 == 0)
 || (eAILevel == enuAILevel.average && (int)(MainForm.rnd.NextDouble() * 1000) % 10 == 0)
 || (eAILevel == enuAILevel.bright && (int)(MainForm.rnd.NextDouble() * 1000) % 2 == 0)
 || (eAILevel == enuAILevel.genius ))
{
    int intCountNumCardsKnown = 0;
    for (enuCards eCardCounter = (enuCards)0; 
              eCardCounter < enuCards._numCards; eCardCounter++)
    {
        if (udrAILogic.eNotes[(int)eSuspect, (int)eCardCounter] == enuNote.check)
            intCountNumCardsKnown++;
    }
    if (intCountNumCardsKnown == MainForm.cSuspects[(int)eSuspect].cCards.Length)
    {
        // all of this suspect's cards have been identified
        //        -> X the ones that are unknown for this suspect
        for (enuCards eCardCounter = 
              (enuCards)0;eCardCounter < enuCards._numCards; eCardCounter++)
        {
            if (udrAILogic.eNotes[(int)eSuspect,(int)eCardCounter] != enuNote.check)
                XNote(eCardCounter, eSuspect);
        }
    }
}

Graphics and the Hoopla

You might have noticed the pretty effects which make this game kind of cool. Some of them you've seen before (if you've read or downloaded some of my previous articles) like Jessica Rabbit in the title animation. She's animated using classSprite which I discussed in a previous article: Sprite Editor for .NET. For anyone who has made use of this class, you will be glad to know that I've made a few upgrades, the most important of which is the accelerated load time. The major draw-back of the class was that it took so long to load it became tedious to use. But this new upgrade is backwardly compatible, and automatically upgrades the sprite files from .spr to .sp2, so that it will reduce the load time by a factor of 10 (at least!). Just download this Clue code, copy classSprite from it, and stick it wherever.

As for the other pretty things, there isn't anything quite as pretty as Jessica Simpson. Er... uhh... forgive the freudian (I'm an Ashlee fan myself!), there isn't anything quite as pretty as Jessica Rabbit, the cards themselves are kinda cool. Like I mentioned earlier, all the images in this project were downloaded off the internet and modified. And when I decided I wanted to animate the process of sorting the cards, shuffling the three different types, and picking the mystery cards before dealing the remaining ones, I had to generate 'sprites' for each card. This seemed unnecessary, so I created a new type called classRotatedImage. This class takes an input image, rotates it three times, and stores four base images in an .rbmp file on the hard-drive. Doing this takes time, and that is the reason why the first time you load the game, it will take up to two minutes before you can see the intro-animation, but once these cards are generated, the load time is much much faster. These four base-images are rotated evenly between zero and pi/2 radians (or thereabouts) so that .NET's native Bitmap's FlipRotate function can then be used to generate the remaining 12 of the total rotated 16 images of that particular instance of the class.

Don't flip out

flipping_the_cards.PNG

The single most difficult part of this project was getting the cards to flip during the deal-cards animation. This is the part where the cards have been sorted into categories then spread out face-up. The idea was to create an effect that would give the illusion of these two dimensional images flipping in a cascade of cards, much like a magician might by spreading the cards out onto the table and then flip the first card and watch the rest of them follow. Creating the flip-one-card illusion is just a matter of slimming the image to an appropriate size during the animation, then widening it until it is normal size again on the reverse side. But getting a cascade of cards to do that at the same time was a bit more difficult, and it was done in the main form's:

C#
AnimateDealCards_FlipCardsUtoD_DirR(enuCardType eTypeCard)

function, which is called by:

C#
animateDealCards()

When the tmrDelay's tick event handler trips, eMode is in dealCards mode, and the variable eDealMode is set to flipWeaponcards, flipSuspectcards, or flipRoomcards.

The idea was to simulate something like what a magician might do when he spreads a pack of cards out onto the table, then flips the first one and watches the rest of them flip after it. To do this, the program spreads the cards out and retains their locations on the table. Then it moves an invisible finger across the breadth of the entire spread. Each card has one edge which remains in contact with the table; the point at which this edge makes contact with the table is called that card's pivot-point. The left-most card that still has its pivot point to the right of the invisible finger is designated the pivot-card at any instance during the card-flip animation. This is the furthest card to the left that is still face-up. It is drawn first, then the next one to its right, then the one after, and so on until the last card to the right has been drawn, and the animation then proceeds to draw the cards to the left of the pivot point one after another, starting from the card nearest to the pivot card until it finishes with the card furthest to the left of the spread.

But before drawing any of these cards, their relative image widths need to be calculated. To do this, the algorithm loops while looking at the cards from a side view.

flipping_the_cards___side_view.png

To calculate the width of card B, it:

  1. knows the location of card A's floating edge (x, y) above
  2. the distance between card A's pivot point and card B's pivot point
  3. it calculates the contact point between cards A and B
  4. it then calculates the angle card B makes with the table
  5. and then knows the desired width of the output image of card B

Since the contact point of one card is used in the next's card's calculations as the 'floating edge' by this algorithm, it can easily cycle through all the cards one after the other. The sizes of each of the face-down cards (those to the left of the pivot card) are calculated in much the same way, but in reverse, with negated x values to simplify things. However, the card immediately to the left of the pivot card can either have its edge pressed against the side of the pivot card, or it can have its side pressed against the edge of the pivot card and, after a week of flipping out about this (in a very cool, calm, and dignified manner), I decided that approximating this by setting both of their floating edge's X values to the invisible finger's position was going to be just good enough (this isn't the guidance system of a heat-seeking missile, after all). And that was the end of that.

Behind the Looking Glass

The intro-animation is pretty cool. Yep, pretty cool.

To do this, I used a copy of the board and notebook magnified by a factor of three, and kept it in memory. This image is much too large to run the animation smoothly, so it is then cut up into an array of much smaller images, and the whole bit-of-the-map thing is stored in this array, using about twice as much memory than before, but each smaller image can be accessed and used much more easily by the algorithm that draws the magnifying glass and the magnified image behind it onto the screen. The width of these parcels of the whole are sized by calculating the smallest value, which divides evenly into the width of the entire magnified image, and is also bigger than twice the width of the magnifying glass' lens. The height is done in a similar fashion.

Christ_Kennedy.PNG

The magnifying glass lens image consists of

  1. the rim of the lens,
  2. a red-colored area around the rim of the lens
  3. and a white colored area inside the lens

This magnifying glass is moved around the magnified image. When its location is determined, after it has been moved, its surface area is described (the actual position of the lens is stored in memory as the position of the center of the lens, so that the area which the lens covers needs to be calculated). This surface area is then copied from one of the parcel images, stored in the area described in the previous paragraph, onto a new bitmap the size of the lens. The area inside the lens's rim is made transparent, and then the lens is superimposed onto the new bitmap of the magnified board we've just created so as to make the magnified image appear as if it were "behind the lens". Then the red area around the rim of the lens is made transparent, and this new image, rim, and magnified game-board behind the lens are drawn onto a regular sized copy of the board and notebook before going to the screen.

And that would be that if there weren't any protesting rats and spider-webs. And what's this Waldo dude doing here anyway? But then things can always be improved, and so, that is the reason why we need to throw in a bit of animation. The animated characters are all done using the new classSprite, and they do their thing whether or not anyone is watching them (their respective intConfiguration and intConfigurationStep values are updated at each iteration of the intro animation). Their images are generated and put to the screen whenever the magnifying glass comes close enough for them to be needed. These sprite images are placed onto the copy of the mega-board images before the magnifying glass' lens is drawn and then placed onto a bitmap of the regular sized board.

When all that is done, the magnifying glass' handle is drawn onto the screen, and the whole animation just loops until the user begins a new game.

Bring Forward the Accused

When the user (or the computer AI geniusified, dullified, or stupidated) finally makes an accusation, we roll ourselves into the I accuse mode of things. This is a bit of neat code in that it creates a reasonably cool effect. There are two images of everything: one in color and another in black/white. The moving parts, aside from the floodlight, are in the foreground, and the background is nothing but background. The black/white background, however, does have a colored "I accuse" accusing suspect and speaking bubble, but aside from that, everything else is in black and white.

accuse_floodlight.PNG

Using a blue-screen (I'm off the green screen, I got angry picketers from Green-Peace outside my door, and I'm afraid what they're going to do), I move my 'floodlight' around by drawing a transparent circle on my blue screen, and then superimpose this onto the colored image (with the animated colored foreground drawn onto the colored background), and cut-out a colored circular segment of that image; then, making the blue part of the resultant image transparent, I draw this onto the black/white image, and repeat the process by moving the floodlight around until we're finally ready to flip the cards and see the Who and How!

But sadly, we may never know why Mr. Boddy was murdered so tragically.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO unemployable
Canada Canada
Christ Kennedy grew up in the suburbs of Montreal and is a bilingual Quebecois with a bachelor’s degree in computer engineering from McGill University. He is unemployable and currently living in Moncton, N.B. writing his next novel.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Guirec15-Nov-12 21:26
professionalGuirec15-Nov-12 21:26 
QuestionMy Vote of 5 Pin
AlirezaDehqani1-Nov-12 0:12
AlirezaDehqani1-Nov-12 0:12 
GeneralMy vote of 5 Pin
Sanjay K. Gupta23-Feb-12 18:01
professionalSanjay K. Gupta23-Feb-12 18:01 
QuestionNice work.... Pin
Jαved26-Dec-11 22:33
professionalJαved26-Dec-11 22:33 
GeneralMy vote of 5 Pin
xExTxCx4-Oct-10 14:13
xExTxCx4-Oct-10 14:13 
GeneralRe: My vote of 5 Pin
Christ Kennedy26-Oct-10 11:03
mvaChrist Kennedy26-Oct-10 11:03 
GeneralOutstanding work Pin
Marcelo Ricardo de Oliveira19-Aug-10 11:02
mvaMarcelo Ricardo de Oliveira19-Aug-10 11:02 
GeneralRe: Outstanding work Pin
Christ Kennedy19-Aug-10 15:30
mvaChrist Kennedy19-Aug-10 15:30 
GeneralException in loadSprite_V1 Pin
SonOfPirate18-Aug-10 14:12
SonOfPirate18-Aug-10 14:12 
GeneralRe: Exception in loadSprite_V1 Pin
Christ Kennedy19-Aug-10 1:37
mvaChrist Kennedy19-Aug-10 1:37 
GeneralRe: Exception in loadSprite_V1 Pin
SonOfPirate19-Aug-10 12:11
SonOfPirate19-Aug-10 12:11 
GeneralRe: Exception in loadSprite_V1 Pin
Christ Kennedy19-Aug-10 15:29
mvaChrist Kennedy19-Aug-10 15:29 
GeneralMy vote of 5 Pin
Emile van Gerwen17-Aug-10 0:31
Emile van Gerwen17-Aug-10 0:31 
GeneralRe: My vote of 5 Pin
Christ Kennedy17-Aug-10 1:59
mvaChrist Kennedy17-Aug-10 1:59 
GeneralRe: My vote of 5 Pin
Emile van Gerwen17-Aug-10 10:02
Emile van Gerwen17-Aug-10 10:02 
GeneralRe: My vote of 5 Pin
Christ Kennedy17-Aug-10 12:33
mvaChrist Kennedy17-Aug-10 12:33 
GeneralRe: My vote of 5 Pin
Emile van Gerwen17-Aug-10 21:19
Emile van Gerwen17-Aug-10 21:19 
GeneralRe: My vote of 5 Pin
Christ Kennedy18-Aug-10 2:07
mvaChrist Kennedy18-Aug-10 2:07 
GeneralMy vote of 5 [modified] Pin
thatraja15-Aug-10 23:06
professionalthatraja15-Aug-10 23:06 
GeneralRe: My vote of 5 Pin
Christ Kennedy16-Aug-10 2:20
mvaChrist Kennedy16-Aug-10 2:20 
GeneralMy vote 5 Pin
Nitin S14-Aug-10 7:02
professionalNitin S14-Aug-10 7:02 
GeneralRe: My vote 5 Pin
Christ Kennedy14-Aug-10 11:50
mvaChrist Kennedy14-Aug-10 11:50 
GeneralMy vote of 4 Pin
Toli Cuturicu14-Aug-10 6:03
Toli Cuturicu14-Aug-10 6:03 
GeneralRe: My vote of 4 Pin
Christ Kennedy14-Aug-10 11:50
mvaChrist Kennedy14-Aug-10 11:50 
GeneralMy vote of 5 Pin
Bigdeak12-Aug-10 21:53
Bigdeak12-Aug-10 21:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.