
// taquin - 4x4 Moving tiles puzzle.
// Author: Po Shan Cheah
// Last updated: August 18, 2003
//
// Compile with: csc /t:winexe taquin.cs
namespace Taquin {
using System;
using System.Drawing;
using System.Windows.Forms;
/// <summary>Override the Button class to make the arrow keys regular
/// input.</summary>
class MyButton : Button {
protected override bool IsInputKey(Keys keyData) {
switch (keyData & Keys.KeyCode) {
case Keys.Up:
case Keys.Down:
case Keys.Left:
case Keys.Right:
return true;
}
return false;
}
}
/// <summary>This is for passing a move function to the push functions in
/// the Board class. The move function can be a regular move or an animated
/// move.</summary>
delegate void MoveProc(int row, int col);
/// <summary>Global constants</summary>
class Params {
// Puzzle dimensions.
public const int ROWS = 4;
public const int COLUMNS = 4;
// Tile size and tile decoration sizes.
public const int TILESIZE = 100;
public const int TILEOUTERBORDER = 5;
public const int TILEINNERBORDER = 15;
}
/// <summary>The game board. Implements simple board
/// functions.</summary>
class Board {
/// <summary>The game board itself.</summary>
/// <remarks>Each array element is the number of the tile occupying
/// that space. A blank tile is represented by a 0 in that array
/// element.</remarks>
int[,] board = new int[Params.ROWS, Params.COLUMNS];
// Location of the blank square.
int blankrow;
int blankcol;
public Board() {
NormalReset();
}
/// <value>Row number of the blank tile.</value>
public int BlankRow {
get { return blankrow; }
}
/// <value>Column number of the blank tile.</value>
public int BlankCol {
get { return blankcol; }
}
/// <summary>Reset to normal configuration. Tiles in correct
/// order.</summary>
public void NormalReset() {
for (int i = 1; i < Params.ROWS * Params.COLUMNS; ++i)
board[(i - 1) / Params.ROWS, (i - 1) % Params.COLUMNS] = i;
board[Params.ROWS - 1, Params.COLUMNS - 1] = 0;
blankrow = Params.ROWS - 1;
blankcol = Params.COLUMNS - 1;
}
/// <summary>Same as normal configuration but with the last two
/// tiles swapped.</summary>
/// <remarks>The idea is you can't get from this
/// configuration to the normal configuration so scrambling this
/// one would make an impossible puzzle.</remarks>
public void InvertReset() {
NormalReset();
board[Params.ROWS - 1, Params.COLUMNS - 3] =
Params.ROWS * Params.COLUMNS - 1;
board[Params.ROWS - 1, Params.COLUMNS - 2] =
Params.ROWS * Params.COLUMNS - 2;
}
/// <summary>Set board position <paramref name="row"/> and
/// <paramref name="col"/> to be a blank tile.</summary>
public void SetBlank(int row, int col) {
board[row, col] = 0;
blankrow = row;
blankcol = col;
}
/// <summary>Move the blank tile to <paramref name="row"/> and
/// <paramref name="col"/>.</summary>
public void MoveBlank(int row, int col) {
board[blankrow, blankcol] = board[row, col];
SetBlank(row, col);
}
/// <summary>Push a tile up into the blank space.</summary>
/// <param name="move">The move function to use. This can be a
/// regular move or an animated move.</param>
public void PushUp(MoveProc move) {
if (blankrow < Params.ROWS - 1)
move(blankrow + 1, blankcol);
}
/// <summary>Push a tile down into the blank space.</summary>
/// <param name="move">The move function to use. This can be a
/// regular move or an animated move.</param>
public void PushDown(MoveProc move) {
if (blankrow > 0)
move(blankrow - 1, blankcol);
}
/// <summary>Push a tile left into the blank space.</summary>
/// <param name="move">The move function to use. This can be a
/// regular move or an animated move.</param>
public void PushLeft(MoveProc move) {
if (blankcol < Params.COLUMNS - 1)
move(blankrow, blankcol + 1);
}
/// <summary>Push a tile right into the blank space.</summary>
/// <param name="move">The move function to use. This can be a
/// regular move or an animated move.</param>
public void PushRight(MoveProc move) {
if (blankcol > 0)
move(blankrow, blankcol - 1);
}
/// <value>Indexer enables transparent access to board
/// array.</value>
public int this[int row, int col] {
get {
return board[row, col];
}
set {
board[row, col] = value;
}
}
/// <summary>Scramble the board by making random moves.</summary>
/// <remarks>Note that we can't simply shuffle the tiles because
/// there are boards that are unreachable with any number
/// of moves.</remarks>
public void Scramble() {
Random rand = new Random();
MoveProc move = new MoveProc(MoveBlank);
for (int i = 0; i < 200; ++i) {
switch (rand.Next(4)) {
case 0:
PushUp(move);
break;
case 1:
PushDown(move);
break;
case 2:
PushLeft(move);
break;
case 3:
PushRight(move);
break;
}
}
}
/// <summary>Returns true if this tile is next to a blank
/// space in any direction.</summary>
public bool IsNextToBlank(int row, int col) {
return row == blankrow &&
(col == blankcol - 1 || col == blankcol + 1) ||
col == blankcol &&
(row == blankrow - 1 || row == blankrow + 1);
}
} // class Board
/// <summary>Class for animating a tile.</summary>
class AnimateTile {
const int STEPS = 5;
int step;
int newx;
int newy;
int oldx;
int oldy;
int newrow;
int newcol;
int tile;
Panel canvas;
Board board;
public AnimateTile(Panel canvas, Board board,
int oldrow, int oldcol,
int newrow, int newcol) {
this.canvas = canvas;
this.board = board;
oldx = oldcol * Params.TILESIZE;
oldy = oldrow * Params.TILESIZE;
newx = newcol * Params.TILESIZE;
newy = newrow * Params.TILESIZE;
this.newrow = newrow;
this.newcol = newcol;
tile = board[oldrow, oldcol];
board.SetBlank(oldrow, oldcol);
step = 0;
}
/// <summary>Draw a tile in between two squares.</summary>
public void DrawTween(BoardPainter boardPainter) {
boardPainter.DrawTile(oldx + (newx - oldx) / STEPS * step,
oldy + (newy - oldy) / STEPS * step,
tile.ToString());
}
/// <summary>Advance the animation by one step.</summary>
/// <remarks>If the animation is done, set the destination board
/// space to the tile number that just moved in.</remarks>
public void Tick() {
if (step < STEPS) {
++step;
// Invalidate only a 2x2 area to reduce update flicker.
// This could be further reduced to a 1x2 area but it would
// then depend on the direction of movement and isn't worth
// the extra code.
canvas.Invalidate(new Rectangle(Math.Min(oldx, newx),
Math.Min(oldy, newy),
2 * Params.TILESIZE,
2 * Params.TILESIZE));
canvas.Update();
}
if (Done())
board[newrow, newcol] = tile;
}
/// <summary>Returns true if the animation is finished.</summary>
public bool Done() {
return step >= STEPS;
}
} // class AnimateTile
/// <summary>Functions for drawing tiles on the game board.</summary>
class BoardPainter {
Graphics g;
static Font font = new Font("SansSerif", 24, FontStyle.Bold);
static Brush foreBrush = new SolidBrush(Color.LightGray);
static Brush backBrush = new SolidBrush(Color.Black);
public BoardPainter(Graphics g) {
this.g = g;
}
/// <summary>Displays text centered on a specific coordinate.
/// Takes into account text width and height.</summary>
/// <param name="text">Text to be drawn</param>
/// <param name="x">Where to draw the text. (pixel position)</param>
/// <param name="y">Where to draw the text. (pixel position)</param>
void CenterText(string text, int x, int y) {
SizeF size = g.MeasureString(text, font);
g.DrawString(text, font, foreBrush,
new PointF(x - size.Width / 2, y - size.Height / 2));
}
/// <summary>Draw a blank tile.</summary>
/// <param name="x">Where to draw the tile. (pixel
/// position)</param>
/// <param name="y">Where to draw the tile. (pixel
/// position)</param>
public void DrawBlank(int x, int y) {
g.FillRectangle(backBrush, x, y, Params.TILESIZE, Params.TILESIZE);
}
/// <summary>Draw a tile.</summary>
/// <param name="x">Where to draw the tile. (pixel
/// position)</param>
/// <param name="y">Where to draw the tile. (pixel
/// position)</param>
/// <param name="tilestr">Text to draw in the tile</param>
public void DrawTile(int x, int y, string tilestr) {
DrawBlank(x, y);
g.FillRectangle(foreBrush,
x + Params.TILEOUTERBORDER,
y + Params.TILEOUTERBORDER,
Params.TILESIZE - Params.TILEOUTERBORDER * 2,
Params.TILESIZE - Params.TILEOUTERBORDER * 2);
g.FillRectangle(backBrush,
x + Params.TILEINNERBORDER,
y + Params.TILEINNERBORDER,
Params.TILESIZE - Params.TILEINNERBORDER * 2,
Params.TILESIZE - Params.TILEINNERBORDER * 2);
CenterText(tilestr, x + Params.TILESIZE / 2, y + Params.TILESIZE / 2);
}
} // class BoardPainter
class Taquin : Form {
Panel canvas;
Board board = new Board();
const int DELAY = 10;
AnimateTile animateTile = null;
Timer animateTimer = null;
/// <summary>Timer event handler for the animation.</summary>
void AnimateTick(Object o, EventArgs e) {
if (animateTile == null)
return;
animateTile.Tick();
if (animateTile.Done()) {
animateTile = null;
animateTimer.Stop();
animateTimer.Dispose();
animateTimer = null;
}
}
/// <summary>Animated version of MoveBlank().</summary>
/// <seealso cref="Board.MoveBlank"/>
void MoveBlankAnimate(int row, int col) {
animateTile = new AnimateTile(canvas, board,
row, col,
board.BlankRow, board.BlankCol);
animateTimer = new Timer();
animateTimer.Tick += new EventHandler(AnimateTick);
animateTimer.Interval = DELAY;
animateTimer.Start();
}
/// <summary>Draw the entire game board.</summary>
/// <remarks>Also draws the in-between tile if an animation is in
/// progress.</remarks>
void DrawBoard(Graphics g) {
BoardPainter boardPainter = new BoardPainter(g);
for (int i = 0; i < Params.ROWS; ++i) {
for (int j = 0; j < Params.COLUMNS; ++j) {
int tile = board[i, j];
if (tile == 0)
boardPainter.DrawBlank(j * Params.TILESIZE,
i * Params.TILESIZE);
else
boardPainter.DrawTile(j * Params.TILESIZE,
i * Params.TILESIZE,
tile.ToString());
}
}
if (animateTile != null)
animateTile.DrawTween(boardPainter);
}
/// <summary>Event handler for the Paint event.</summary>
void canvas_Paint(object o, PaintEventArgs e) {
DrawBoard(e.Graphics);
}
/// <summary>Event handler for the MouseDown event.</summary>
/// <remarks>Makes move if a click falls on a movable tile.</remarks>
void canvas_MouseDown(object o, MouseEventArgs e) {
if (animateTile != null)
return;
int clickrow = e.Y / Params.TILESIZE;
int clickcol = e.X / Params.TILESIZE;
if (board.IsNextToBlank(clickrow, clickcol))
MoveBlankAnimate(clickrow, clickcol);
}
/// <summary>Event handler for the KeyDown event.</summary>
/// <remarks>For moving tiles with the arrow keys.</remarks>
protected override void OnKeyDown(KeyEventArgs e) {
if (animateTile != null)
return;
switch (e.KeyCode) {
case Keys.Up:
board.PushUp(new MoveProc(MoveBlankAnimate));
e.Handled = true;
break;
case Keys.Down:
board.PushDown(new MoveProc(MoveBlankAnimate));
e.Handled = true;
break;
case Keys.Left:
board.PushLeft(new MoveProc(MoveBlankAnimate));
e.Handled = true;
break;
case Keys.Right:
board.PushRight(new MoveProc(MoveBlankAnimate));
e.Handled = true;
break;
}
}
/// <summary>Click event handler for the Normal Reset button.</summary>
void normal_Click(object o, EventArgs e) {
board.NormalReset();
canvas.Invalidate();
canvas.Update();
}
/// <summary>Click event handler for the Inverted Reset button.</summary>
void invert_Click(object o, EventArgs e) {
board.InvertReset();
canvas.Invalidate();
canvas.Update();
}
/// <summary>Click event handler for the Scramble button.</summary>
void scramble_Click(object o, EventArgs e) {
board.Scramble();
canvas.Invalidate();
canvas.Update();
}
Taquin() {
Text = "Taquin";
Name = "Taquin";
// Don't allow maximizing the window.
MaximizeBox = false;
// Don't allow resizing the window.
FormBorderStyle = FormBorderStyle.FixedSingle;
// Activate double-buffering.
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);
Button normalButton = new MyButton();
normalButton.Text = "Normal Reset";
normalButton.Size = new Size(100, 25);
Button invertButton = new MyButton();
invertButton.Text = "Inverted Reset";
invertButton.Location = new Point(100, 0);
invertButton.Size = new Size(100, 25);
Button scrambleButton = new MyButton();
scrambleButton.Text = "Scramble";
scrambleButton.Location = new Point(200, 0);
scrambleButton.Size = new Size(100, 25);
canvas = new Panel();
canvas.Location = new Point(0, 25);
canvas.Size = new Size(Params.COLUMNS * Params.TILESIZE,
Params.ROWS * Params.TILESIZE);
canvas.Paint += new PaintEventHandler(canvas_Paint);
canvas.MouseDown += new MouseEventHandler(canvas_MouseDown);
Controls.Add(normalButton);
Controls.Add(invertButton);
Controls.Add(scrambleButton);
Controls.Add(canvas);
ClientSize = new Size(Params.COLUMNS * Params.TILESIZE,
Params.ROWS * Params.TILESIZE + 25);
// Capture key events in the Form before they are passed to the
// current control.
KeyPreview = true;
normalButton.Click += new EventHandler(normal_Click);
invertButton.Click += new EventHandler(invert_Click);
scrambleButton.Click += new EventHandler(scramble_Click);
}
static int Main() {
Application.Run(new Taquin());
return 0;
}
}
} // namespace Taquin
// The End