Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

JawBreaker Game in C#

0.00/5 (No votes)
28 Dec 2003 1  
A simple implementation of Jawbreaker

Introduction

This is a C# implementation of JawBreaker, A free game shipped with Pocket PC 2003.

Background

After playing Jawbreaker on my pocket pc for couple of days, I was having some issues with the program. One was it allowed only one Undo and the board had a fixed number of cells. I also wanted to play this game on my PC. So I decided to write the program in C#. I never programmed a game before. So I used this as a learning experience. The game has no AI and very simple to implement (Caution: It is very addictive).

Using the code

The zip file contains source code that was compiled for Visual Studio.NET 2003. Just open the solution file and compile it. The executable created is called JawBreaker.exe. You could play the game very easily. All you have to do is get as many similar cells together and then remove them (The more the cells, the higher the score). The first click selects the block of similar neighbor cells recursively. If you click anywhere on the selected block the entire block is removed. The objective of the game is to score as many points as possible.

Program Design

The program is organized into 3 classes:

  • Cell
  • Board
  • MainForm

Cell

Cell is a representation of a cell on the Board . Every cell has a type. The type is what determines its color.

    int row;
    int col;
    int type;

    public Cell(int row,int col,int type)
    {
        this.row=row;
        this.col=col;
        this.type=type;
    }

Board

The Board class is what stores the game state. It has a two dimensional array of Cell objects. It also has two Stack objects for storing the Undo and Redo information. The totalScore has the current score.

// Members of the Board class

    private int rows;
    private int cols;
    private int types;
    private Cell[,] data;
    private ArrayList selected;
    private Stack undoStack;
    private Stack redoStack;
    private int totalScore;

When cells are removed, those locations are set to be null. The above member variables explain the state of the game. The variable totalScore is what maintains the total score of the board.

  • public void Remove(ArrayList arl)
  • public void Select(Cell c)
  • public bool Undo()
  • public bool Redo()
  • public void Initialize()
  • public bool GameFinished()

The function Select(Cell c) will select the neighbourhood cells recursively for the given Cell.

  //Selects the neighborhood Cells recursively


    public void Select(Cell c)
    {
        selected.Add(c);
        ArrayList arl=GetNeighborhoodCells(c);
        if(arl.Count==0)
        {
            return;
        }
        foreach(Cell cell in arl)
        {
            if(cell.Type==c.Type&&!selected.Contains(cell))
            {
                Select(cell);
            }
        }
    }

The function Remove(ArrayList arl) will remove cells that are sent in the ArrayList. It will make those locations as null .

    // Removes cells from the board


    public void Remove(ArrayList arl)
    {
        if(arl.Count>0)
        {
            //Fist push the current data to Undo Stack


            undoStack.Push(GetTypeArray());

            //Clear the redo Stack..


            redoStack.Clear();




            foreach(Cell c in arl)
            {
                data[c.Row,c.Col]=null;
            }
            //Sync the board

            Syncronize();

            //update the score


            totalScore+=GetScore(arl.Count);
        }
    }

The function bool Undo() will set the current boards state with the popped element from the Undo Stack. If the undo succeeds it returns true.

    public bool Undo()
    {
        if(undoStack.Count>0)
        {
            selected.Clear();
            //Push to redo

            int[,] current=GetTypeArray();
            int[,] popped=(int[,])undoStack.Pop();

            //update the scores

            totalScore-=GetScore(
                 GetActiveCells(popped)-GetActiveCells(current));

            redoStack.Push(current);
            SetTypeArray(popped);
            return true;
        }

        return false;

    }

The function bool Redo() will set the current boards state with the popped element from the Redo Stack. If the redo succeeds it returns true.

    public bool Redo()
    {
        if(redoStack.Count>0)
        {
            selected.Clear();
            int[,] current=GetTypeArray();
            int[,] popped=(int[,])redoStack.Pop();

            //update score

            totalScore+=GetScore(GetActiveCells(
                  current)-GetActiveCells(popped));

            //push current to undo

            undoStack.Push(current);

            //pop for redo stack

            SetTypeArray(popped);
            return true;
        }

        return false;


    }

The function Initialize() initializes the board with Random Cells, sets the score to zero and clears the Undo and Redo Stacks.

    public void Initialize()
    {
        Random r=new Random();


        for(int i=0;i<rows;i++)
        {
            for(int j=0;j<cols;j++)
            {
                data[i,j]=new Cell(i,j,r.Next(types));
            }
        }

        totalScore=0;
        undoStack.Clear();
        redoStack.Clear();
        selected.Clear();

    }

The function GameFinished() checks whether there are any moves left in the board. If there are any it returns false, else it returns true.

    //Checks if all the moves are finished

    public bool GameFinished()
    {
        for(int i=0;i<rows;i++)
        {
            for(int j=0;j<cols;j++)
            {
                Cell c=data[i,j];
                if(c!=null)
                {
                    ArrayList arl=GetNeighborhoodCells(c);
                    foreach (Cell ce in arl)
                    {
                        if(c.Type==ce.Type)
                            return false;
                    }
                }

            }
        }

        return true;

    }

MainForm

MainForm is where most of the drawing takes place. There are two drawing methods.

  • public void DrawBall(Graphics grfx,Rectangle rect,Color c,bool Selected)
  • public void DrawBoard(Graphics grfx)

The DrawBall(..) method draws a ball on the given graphics object. It is bounded by the rectangle and is of the color passed to it. If Selected is true, it draws a background before drawing the ball as you see in the following code

    public void DrawBall(Graphics grfx,Rectangle rect,Color c,bool Selected)
    {
        if(Selected)
        {
            grfx.FillRectangle(Brushes.Goldenrod,rect);
        }
        GraphicsPath path=new GraphicsPath();

        path.AddEllipse(rect);

        PathGradientBrush pgbrush= new PathGradientBrush(path);
        pgbrush.CenterPoint=new Point((rect.Right- rect.Left)
           /3+rect.Left,(rect.Bottom - rect.Top) /3+rect.Top);
        pgbrush.CenterColor=Color.White;
        pgbrush.SurroundColors=new Color[] { c };

        grfx.FillRectangle(pgbrush,rect);
        grfx.DrawEllipse(new Pen(c),rect);

    }

The method DrawBoard is called whenever a board refresh is needed. Right now it just redraws the entire screen with the current contents of the board. Also this is the method that determines which color is used for each type of the cell. Right now it can have upto 5 colors, but this could be easily increased by modifying this function.

    public void DrawBoard(Graphics grfx)
    {
	//Do Double Buffering

	Bitmap offScreenBmp;
	Graphics offScreenDC;
	offScreenBmp = new Bitmap(boardPanel.Width, boardPanel.Height);
	offScreenDC = Graphics.FromImage(offScreenBmp);

	offScreenDC.Clear(boardPanel.BackColor);
	offScreenDC.SmoothingMode=SmoothingMode.AntiAlias;

	int height=boardPanel.Height-1;
	int width=boardPanel.Width-1;

	int rectHeight=(int)Math.Round((double)height/b.Cols);
	int rectWidth=(int)Math.Round((double)width/b.Rows);



	int x=0;
	int y=0;
	ArrayList arl=b.Selected;


	for(int i=0;i<b.Rows;i++)
	{
		x=0;
		for(int j=0;j<b.Cols;j++)
		{
			Rectangle r=new Rectangle(x,y,rectWidth-2,rectHeight-2);

			Cell c=b.Data[i,j];
			if(c!=null)
			{
				if(arl.Contains(c))
				{

					DrawBall(offScreenDC,r,colors[c.Type],true);
				}
				else
				{
					DrawBall(offScreenDC,r,colors[c.Type],false);
				}
			}
			else
			{
				//Do nothing

			}

			x+=rectWidth;
		}
		y+=rectHeight;
	}

	if(arl.Count>0)
	{
		//Select the first cell(The cell the user clicked on first)

		Cell c=(Cell)arl[0];

		//Calculate the x and y coordinates

		int x1=c.Col*rectWidth;
		int y1=c.Row*rectHeight;

		//Show the Score..s

		Rectangle scr=new Rectangle(x1,y1,30,30);
		//offScreenDC.F

		StringFormat sf=new StringFormat();
		sf.Alignment=StringAlignment.Center;
		sf.LineAlignment=StringAlignment.Center;
		offScreenDC.DrawString(b.CurrentSelection.ToString(),
                    new Font("Arial",8,FontStyle.Bold),Brushes.Black,scr,sf);

	}

	grfx.DrawImageUnscaled(offScreenBmp, 0, 0);

}

User moves

User moves are handled by the MouseDown event. It calculates the row and column and determines the cell based on the region where the mouse is clicked. Then it calls the select method of the Board. If the current cell is already in the selected items, it means the user wants to remove the cells that are currently selected.

    private void boardPanel_MouseDown(object sender, MouseEventArgs e)
    {

        int x=e.X;
        int y=e.Y;

        int height=boardPanel.Height-1;
        int width=boardPanel.Width-1;

        int rectHeight=(int)Math.Round((double)height/b.Cols);
        int rectWidth=(int)Math.Round((double)width/b.Rows);


        int col=(int)x/rectWidth;
        int row=(int)y/rectHeight;

        ArrayList selected=b.Selected;

        Cell currentCell=b.Data[row,col];
        if(currentCell!=null)
        {

            if(selected.Contains(currentCell))
            {

                expectedScore.Text="Current Selection: 0";

                b.Remove(selected);
                UpdateScore();

            }
            else
            {
                b.Selected.Clear();
                b.Select(currentCell);
                expectedScore.Text="Current Selection: "+
                      GetScore(b.Selected.Count);
            }


        }
        else
        {
            //He clicked on a null Cell -> Clear the selection


            b.Selected.Clear();
            expectedScore.Text="Current Selection: 0";
        }

        DrawBoard(boardPanel.CreateGraphics());
        if(b.GameFinished())
        {


            if(MessageBox.Show(this,"Game Over!!. Your total Score was: "+
                      b.Score+"\nDo you want to Start a New Game?",
                      "JawBreaker",MessageBoxButtons.YesNo,
                      MessageBoxIcon.Exclamation)==DialogResult.Yes)
            {
                NewGame();
            }

        }


    }

After every move it checks for GameFinished, if game is finished, it gives an option of starting a new game.

Points of Interest

I did a quick and dirty Syncronize() method that is called from the Remove(ArrayList) method of the Board class. This might affect performance of the game. Also instead of drawing the entire board, it might be more efficient to draw in the regions that are affected. This game is very interesting in AI point of view. There can be many solutions and finding the optimal solution might be a good challenge.

History

  • December 29, 2003
    • Some additional UI enhancements
  • December 22, 2003
    • Initial Version 1.0

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here