Click here to Skip to main content
Email Password   helpLost your password?

CSharpSnooker

Introduction

This article is intended to share some nice discoveries of writing a pool game in C#. Although my first motivation is to give readers some useful programming information, I'm also hoping you really enjoy the game itself.

Background

The game is built over three cornerstones:

The Game

Rules

The game itself is a simplified snooker game. Instead of 15 red balls, it has only 6. Each red ball grants 1 point, while the "color" balls grant from 2 to 7 points (Yellow=2, Green=3, Brown=4, Blue=5, Pink=6, Black=7).

The player must use the cue ball (white ball) to aim to pot the "ball on". The "ball on" is always alternating between a red ball and a color ball, as long as there are still red balls on the table. Once all red balls are potted, the ball on is the less valuable color ball. If the player misses the ball on, or hits another ball other than the ball on, it is a fault. If the player pots a ball other than the ball on, it is a fault. If the player fails to hit any other ball with the cue ball, it is a fault. The player will only score if there are no faults. The fault points are granted to the opponent. The game is over when all balls are potted (except for the cue ball).

int strokenBallsCount = 0;
foreach (Ball ball in strokenBalls)
{
    //causing the cue ball to first hit a ball other than the ball on
    if (strokenBallsCount == 0 && ball.Points != currentPlayer.BallOn.Points)
        currentPlayer.FoulList.Add((currentPlayer.BallOn.Points < 4 ? 4 :
                                    currentPlayer.BallOn.Points));

    strokenBallsCount++;
}

//Foul: causing the cue ball to miss all object balls
if (strokenBallsCount == 0)
    currentPlayer.FoulList.Add(4);

foreach (Ball ball in pottedBalls)
{
    //causing the cue ball to enter a pocket
    if (ball.Points == 0)
        currentPlayer.FoulList.Add(4);

    //causing a ball not on to enter a pocket
    if (ball.Points != currentPlayer.BallOn.Points)
        currentPlayer.FoulList.Add(currentPlayer.BallOn.Points < 4 ? 4 :
                                   currentPlayer.BallOn.Points);
}

if (currentPlayer.FoulList.Count == 0)
{
    foreach (Ball ball in pottedBalls)
    {
        //legally potting reds or colors
        wonPoints += ball.Points;
    }
}
else
{
    currentPlayer.FoulList.Sort();
    lostPoints = currentPlayer.FoulList[currentPlayer.FoulList.Count - 1];
}

currentPlayer.Points += wonPoints;
otherPlayer.Points += lostPoints;

User Interface

There are three important areas on the screen: the pool, the score, and the cue control.

The Pool

Figure 1. Game pool displaying its many borders in yellow.

The table is a mahogany model, covered with fine blue baize. There are six pockets, one for each corner, and two more in the middle of the long sides.

When it is your turn, when you move the mouse over the table, the mouse pointer takes the form of a target (when the ball on is already selected) or a hand (when you must select a ball on). When you hit the left mouse button, the cue ball will run from its original point to the select point.

void HitBall(int x, int y)
{
    //Reset the frames and ball positions
    ClearSequenceBackGround();
    ballPositionList.Clear();

    poolState = PoolState.Moving;
    picTable.Cursor = Cursors.WaitCursor;

    //20 is the maximum velocity
    double v = 20 * (currentPlayer.Strength / 100.0);

    //Calculates the cue angle, and the translate velocity (normal velocity)
    double dx = x - balls[0].X;
    double dy = y - balls[0].Y;
    double h = (double)(Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2)));
    double sin = dy / h;
    double cos = dx / h;
    balls[0].IsBallInPocket = false;
    balls[0].TranslateVelocity.X = v * cos;
    balls[0].TranslateVelocity.Y = v * sin;
    Vector2D normalVelocity = balls[0].TranslateVelocity.Normalize();

    //Calculates the top spin/back spin velocity,
    //in the same direction as the normal velocity, but in opposite angle
    double topBottomVelocityRatio =
        balls[0].TranslateVelocity.Lenght() * (targetVector.Y / 100.0);
    balls[0].VSpinVelocity = new Vector2D(-1.0d * topBottomVelocityRatio *
             normalVelocity.X, -1.0d * topBottomVelocityRatio * normalVelocity.Y);

    //xSound defines if the sound is coming from the left or the right
    double xSound = (float)(balls[0].Position.X - 300.0) / 300.0;
    soundTrackList[snapShotCount] = @"Sounds\Shot01.wav" + "|" + xSound.ToString();

    //Calculates the ball positions as long as there are moving balls
    while (poolState == PoolState.Moving)
        MoveBalls();

    currentPlayer.ShotCount++;
}

The Score

The score is a vintage wooden panel that shows the two players' scores. In addition, it also shows a blinking image of the ball on.

private void timerBallOn_Tick(object sender, EventArgs e)
{
    if (playerState == PlayerState.Aiming || playerState == PlayerState.Calling)
    {
        picBallOn.Top = 90 + (currentPlayer.Id - 1) * 58;
        showBallOn = !showBallOn;
        picBallOn.Visible = showBallOn;
    }
}

Figure 2. Me against the computer.

The Cue Control

The Cue Control is a brushed steel panel, and has two goals: to control the cue strength (the upper red line) and to control the cue ball "spin". You can use the strength bar to give a more precise shot according to the situation. And, the spin control is useful if you know how to do the "top spin" and the "back spin". The "top spin", also known as "follow", increases the cue ball velocity and gives a more open angle when the cue ball hits another ball. The "back spin", on the other hand, decreases the cue ball velocity, and moves back the cue ball the way it came after striking the object ball. This also affects the resulting angle after the hit, and usually makes the cue ball to move in a curve.

Notice: I didn't implement the "side spin", because I thought it would require too much effort and would add little to the article.

Figure 3. Strength control and spin control.

Figure 4. Spin paths.

Figure 5. Different spins in action: normal (no spin), back spin, and top spin.
public void ResolveCollision(Ball ball)
{
    // get the mtd
    Vector2D delta = (position.Subtract(ball.position));
    float d = delta.Lenght();
    // minimum translation distance to push balls apart after intersecting
    Vector2D mtd =
      delta.Multiply((float)(((Ball.Radius + 1.0 + Ball.Radius + 1.0) - d) / d));

    // resolve intersection --
    // inverse mass quantities
    float im1 = 1f;
    float im2 = 1f;

    // push-pull them apart based off their mass
    position = position.Add((mtd.Multiply(im1 / (im1 + im2))));
    ball.position = ball.position.Subtract(mtd.Multiply(im2 / (im1 + im2)));

    // impact speed
    Vector2D v = (this.translateVelocity.Subtract(ball.translateVelocity));
    float vn = v.Dot(mtd.Normalize());

    // sphere intersecting but moving away from each other already
    if (vn > 0.0f)
        return;

    // collision impulse
    float i = Math.Abs((float)((-(1.0f + 0.1) * vn) / (im1 + im2)));
    Vector2D impulse = mtd.Multiply(1);

    int hitSoundIntensity = (int)(Math.Abs(impulse.X) + Math.Abs(impulse.Y));

    if (hitSoundIntensity > 5)
        hitSoundIntensity = 5;

    if (hitSoundIntensity < 1)
        hitSoundIntensity = 1;

    double xSound = (float)(ball.Position.X - 300.0) / 300.0;
    observer.Hit(string.Format(@"Sounds\Hit{0}.wav",
       hitSoundIntensity.ToString("00")) + "|" + xSound.ToString());

    // change in momentum
    this.translateVelocity = this.translateVelocity.Add(impulse.Multiply(im1));
    ball.translateVelocity = ball.translateVelocity.Subtract(impulse.Multiply(im2));
}

The Movie

Figure 6. In-memory frames.

At every shot, a new "movie" is started. The application calculates all movements and makes a list of ball positions as long as there is at least one moving ball on the table. When all balls are still, the ball positions list is used to create the in-memory frames, just like the frames in a movie. When all frames are created, the movie is played, in a smooth and fast way.

void DrawSnapShots()
{
    XmlSerializer serializer = 
     new XmlSerializer(typeof(List<ballposition>));
    string path = 
     Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    using (StreamWriter sw = new StreamWriter(Path.Combine(path,
                             @"Out\BallPositionList.xml")))
    {
        serializer.Serialize(sw, ballPositionList);
    }

    ClearSequenceBackGround();
    int snapShot = -1;

    Graphics whiteBitmapGraphics = null;

    //For each ball, draws an image of that ball 
    //over the pool background image
    foreach (BallPosition ballPosition in ballPositionList)
    {
        if (ballPosition.SnapShot != snapShot)
        {
            snapShot = ballPosition.SnapShot;
            whiteBitmapGraphics = whiteBitmapGraphicsList[snapShot];
        }

        //draws an image of a ball over the pool background image
        whiteBitmapGraphics.DrawImage(balls[ballPosition.BallIndex].Image,
          new Rectangle((int)(ballPosition.X - Ball.Radius),
          (int)(ballPosition.Y - Ball.Radius),
          (int)Ball.Radius * 2, (int)Ball.Radius * 2), 0, 0,
          (int)Ball.Radius * 2, (int)Ball.Radius * 2, GraphicsUnit.Pixel, attr);
    }
}

private void PlaySnapShot()
{
    //Plays an individual frame, by replacing the image of the picturebox with
    //the stored image of a frame
    picTable.Image = whiteBitmapList[currentSnapShot - 1]; ;
    picTable.Refresh();

    string currentSound = soundTrackList[currentSnapShot - 1];

    if (currentSound.Length > 0)
    {
        currentSound += "|0";
        string fileName = currentSound.Split('|')[0];
        Decimal x = -1 * Convert.ToDecimal(currentSound.Split('|')[1]);

        //Plays the sound considering whether the sounds comes from left or right
        soundEngine.Play3D(fileName, 0, 0, (float)x);
    }

    currentSnapShot++;
}

Sound Engine

As I mentioned previously, the game doesn't use the System.Media.SoundPlayer object to play sounds, because each new sound played "cuts" the current sound. This means, you can't hear the sound of a ball falling into a pocket and the sound of two balls colliding at the same time. I solved this with the IrrKlang component. In addition, I also tell the sound engine to play the sound according to the position of the source of the sound. For example, if a ball falls into the upper right pocket, you hear the sound louder at your right ear. If a ball hits another one at the lower corner of the table, you hear the sound coming from the left. There are some cool snooker sounds I found on the internet, and some of them are soft or hard depending on the velocity of the colliding balls:

Figure 7. Sound effects.
if (currentSound.Length > 0)
{
    currentSound += "|0";
    string fileName = currentSound.Split('|')[0];
    Decimal x = -1 * Convert.ToDecimal(currentSound.Split('|')[1]);

    //Plays the sound considering whether the sounds comes from left or right
    soundEngine.Play3D(fileName, 0, 0, (float)x);
}

A.I.

The so called "Ghost balls" play an important role in the game intelligence. When the computer plays in its turn, it is instructed to look for all good "ghost balls", so that it can have more chances of success. Ghost balls are the spots close to the "ball on", that you can aim to, so that the ball should fall into a specific pocket.

private List GetGhostBalls(Ball ballOn)
{
    List ghostBalls = new List();

    int i = 0;
    foreach (Pocket pocket in pockets)
    {
        //distances between pocket and ball on center
        double dxPocketBallOn = pocket.HotSpotX - ballOn.X;
        double dyPocketBallOn = pocket.HotSpotY - ballOn.Y;
        double hPocketBallOn = Math.Sqrt(dxPocketBallOn * 
            dxPocketBallOn + dyPocketBallOn * dyPocketBallOn);
        double a = dyPocketBallOn / dxPocketBallOn;

        //distances between ball on center and ghost ball center
        double hBallOnGhost = (Ball.Radius - 1.0) * 2.0;
        double dxBallOnGhost = hBallOnGhost * (dxPocketBallOn / hPocketBallOn);
        double dyBallOnGhost = hBallOnGhost * (dyPocketBallOn / hPocketBallOn);

        //ghost ball coordinates
        double gX = ballOn.X - dxBallOnGhost;
        double gY = ballOn.Y - dyBallOnGhost;
        double dxGhostCue = balls[0].X - gX;
        double dyGhostCue = balls[0].Y - gY;
        double hGhostCue = Math.Sqrt(dxGhostCue * dxGhostCue + dyGhostCue * dyGhostCue);

        //distances between ball on center and cue ball center
        double dxBallOnCueBall = ballOn.X - balls[0].X;
        double dyBallOnCueBall = ballOn.Y - balls[0].Y;
        double hBallOnCueBall = Math.Sqrt(dxBallOnCueBall * 
            dxBallOnCueBall + dyBallOnCueBall * dyBallOnCueBall);

        //discards difficult ghost balls
        if (Math.Sign(dxPocketBallOn) == Math.Sign(dxBallOnCueBall) && 
        Math.Sign(dyPocketBallOn) == Math.Sign(dyBallOnCueBall))
        {
            Ball ghostBall = new Ball(i.ToString(), null, 
            (int)gX, (int)gY, "", null, null, 0);
            ghostBalls.Add(ghostBall);
            i++;
        }
    }

    return ghostBalls;
}

Some ghost balls may be difficult or impossible to reach, because they lie behind the object ball. These ghost balls are to be discarded by the computer:

//discards difficult ghost balls
if (Math.Sign(dxPocketBallOn) == Math.Sign(dxBallOnCueBall) && 
    Math.Sign(dyPocketBallOn) == Math.Sign(dyBallOnCueBall))
{
    Ball ghostBall = new Ball(i.ToString(), null, (int)gX, (int)gY, "", null, null, 0);
    ghostBalls.Add(ghostBall);
    i++;
}

The computer must then choose one among the remaining ghost balls (sometimes the computer is lucky, sometimes it is not...).

private Ball GetRandomGhostBall(List ballOnList)
{
    Ball randomGhostBall = null;

    List ghostBalls = new List();

    foreach (Ball ballOn in ballOnList)
    {
        List tempGhostBalls = GetGhostBalls(ballOn);
        foreach (Ball ghostBall in tempGhostBalls)
        {
            ghostBalls.Add(ghostBall);
        }
    }

    int ghostBallCount = ghostBalls.Count;
    if (ghostBallCount > 0)
    {
        Random rnd = new Random(DateTime.Now.Second);
        int index = rnd.Next(ghostBallCount);

        randomGhostBall = ghostBalls[index];
    }
    return randomGhostBall;
}

Figure 8. Ghost Balls.

Future Releases

History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralScreenshot in article?
MartinXLord
3:18 7 Jan '10  
Screenshot in the article doesn't match the supplied exe file in the CSharpSnooker_BIN.zip

No one else has commented on this - have I got the wrong file???
GeneralRe: Screenshot in article?
Marcelo Ricardo de Oliveira
3:33 7 Jan '10  
Hi Martin,

For me, they're pretty much the same. You mean the main screenshot, or another one in the article?

marcelo

Take a look at full source code C# Snooker game here in Code Project.

GeneralRe: Screenshot in article?
MartinXLord
3:58 7 Jan '10  
Marcelo,
I mean the mainscreen shot before the article text starts.
GeneralRe: Screenshot in article?
Marcelo Ricardo de Oliveira
4:12 7 Jan '10  
That's strange. I've downloaded the BIN files and launched the EXE. The images are the same. How does it look like in your computer?

Take a look at full source code C# Snooker game here in Code Project.

GeneralRe: Screenshot in article?
MartinXLord
5:53 7 Jan '10  
There are several differences but the most striking one is that
the pool table image is superimposed on a background of the pool table but misaligned.

I could send you a screen-shot if you wish which will be much clearer.
GeneralRe: Screenshot in article?
Marcelo Ricardo de Oliveira
14:30 7 Feb '10  
Hi Martin, since you liked this C# Snooker game, I'd be very glad if you evaluate my new game XNA Snooker Club (see the link below).

Best regards,
Marcelo
Take a look at XNA Snooker Club game here in Code Project.

GeneralWell Explained
Nishanth Anil
1:10 6 Jan '10  
This article is well explained. Smile Keep up the work.

Nishanth Anil

GeneralRe: Well Explained
Marcelo Ricardo de Oliveira
3:16 6 Jan '10  
Thank you Nishanth! There's a new one coming soon, I hope you'll like it more... Smile

Take a look at full source code C# Snooker game here in Code Project.

GeneralExcellent :-)
lemravec
23:10 30 Dec '09  
nice piece of work!
GeneralRe: Excellent :-)
Marcelo Ricardo de Oliveira
6:20 31 Dec '09  
Thanks a lot, lemravec!
Happy New Year! Smile
cheers!

Take a look at full source code C# Snooker game here in Code Project.

GeneralYou make me proud to be Brazilian!
Gerson Freire
23:30 28 Dec '09  
You make me proud to be Brazilian!
As we use to say here: "Mandou muito bem!!"
GeneralRe: You make me proud to be Brazilian!
Marcelo Ricardo de Oliveira
3:15 29 Dec '09  
Thanks a lot, Dude, I'm so glad you liked it!

Valeu Gerson, um abraço! Smile

Take a look at full source code C# Snooker game here in Code Project.

Generaldefine border!!
qwertyuioo
4:17 18 Dec '09  
Could you teach me how to define the borders/walls?
GeneralRe: define border!!
Marcelo Ricardo de Oliveira
4:56 18 Dec '09  
Hi,

In this image you can see how the walls are defined. There are two classes for borders: TableBorder, which defines the horizontal and vertical walls, and DiagonalBorder, which is used at the two sides of each of the corner pockets.

When a ball hit a wall defined by TableBorder, it causes the ball to rebound. As an analogy, imagine a laser beam reflected by a mirror. Now, replace the laser beam by the ball, and the mirror by the wall. The collision angle must be "mirrored" by the wall line (btw, many pool players use their cues to measure the correct angles between the cue ball, the wall and the object ball).

As for the DiagonalBorders, they have a more simple implementation. They only cause the colliding ball to rebound in a specific direction.

Take a look at full source code C# Snooker game here in Code Project.

GeneralGreat Article !!!!!
Aman Bhullar
19:09 17 Dec '09  
Thanks for uploading the article, its great

Regards
Aman Bhullar
www.arlivesupport.com[^]

GeneralRe: Great Article !!!!!
Marcelo Ricardo de Oliveira
0:16 18 Dec '09  
Thanks a lot Aman, glad you like it!

Take a look at full source code C# Snooker game here in Code Project.

GeneralGreat work :)
Srinath G Nath
22:53 15 Dec '09  
Great work Smile

I cant see the stick used to hit the ball, in the sample you attached here.,.
GeneralRe: Great work :)
Marcelo Ricardo de Oliveira
10:44 16 Dec '09  
Thanks, Srinath! Hey, I don't know which stick you're talking about Smile

Take a look at full source code C# Snooker game here in Code Project.

GeneralRe: Great work :)
Srinath G Nath
22:16 16 Dec '09  
The cue ball (White ball).,. And sometimes it vanishes. I cant understand which direction and to which bal am aiming.,.

Visit My site:
blogger.code4asp.net
www.talks4u.com
GeneralRe: Great work :)
Marcelo Ricardo de Oliveira
3:15 17 Dec '09  
Ok, I've just spotted the problem. Will be corrected soon.
Thanks for your feedback, Srinath!

Take a look at full source code C# Snooker game here in Code Project.

General"Out of memory" Exception
MartinXLord
10:06 11 Dec '09  
Encountered after playing several frames in succesion.

"at System.Drawing.Bitmap..ctor(Image original)
at CSharpSnookerUI.frmTable.ClearFramesAndSounds() in C:\Documents and Settings\Martin Lord\My Documents\DownLoads\Code Project\C#Snooker\11-12-09\CSharpSnooker_SRC\CSharpSnookerUI\frmTable.cs:line 198"

Another suggestion:-
When the cue ball is potted (ie a foul)my recollection of the rules is that the next player can place it anywhere within the "D" before playing.

Other thing Sometimes balls are left ovelapping after a shot or over the table boundary

Still addicted!

regards,Martin
GeneralRe: "Out of memory" Exception
Marcelo Ricardo de Oliveira
10:14 11 Dec '09  
Thanks a lot, Martin, I'm fixing it ASAP. And thanks for the suggestion too.
regards
Marcelo

Take a look at full source code C# Snooker game here in Code Project.

GeneralGood work
Bharath K A
7:00 10 Dec '09  
Good work and great article.
GeneralRe: Good work
Marcelo Ricardo de Oliveira
8:04 10 Dec '09  
Thank you, Bharath, I'm glad you like it Smile

Take a look at full source code C# Snooker game here in Code Project.

GeneralException
apdsws
7:35 9 Dec '09  
Hi,

This seems to be very interesting and I am curious run this code on my box but I am getting following error.

"Could not load file or assembly 'irrKlang.NET2.0, Version=1.1.3.0, Culture=neutral, PublicKeyToken=a854741bd80517c7' or one of its dependencies. An attempt was made to load a program with an incorrect format."

at CSharpSnookerUI.frmTable..ctor()
at CSharpSnookerUI.Program.Main() in H:\My Docs\Temp Projects\CSharpSnooker_SRC\CSharpSnookerUI\Program.cs:line 17
at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()

Not sure what to do.


Last Updated 14 Dec 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010