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
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.
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.
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
.
public void Remove(ArrayList arl)
{
if(arl.Count>0)
{
undoStack.Push(GetTypeArray());
redoStack.Clear();
foreach(Cell c in arl)
{
data[c.Row,c.Col]=null;
}
Syncronize();
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();
int[,] current=GetTypeArray();
int[,] popped=(int[,])undoStack.Pop();
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();
totalScore+=GetScore(GetActiveCells(
current)-GetActiveCells(popped));
undoStack.Push(current);
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
.
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)
{
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
{
}
x+=rectWidth;
}
y+=rectHeight;
}
if(arl.Count>0)
{
Cell c=(Cell)arl[0];
int x1=c.Col*rectWidth;
int y1=c.Row*rectHeight;
Rectangle scr=new Rectangle(x1,y1,30,30);
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
{
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