randomfox (randomfox) wrote,
randomfox
randomfox

Taquin (sliding tiles puzzle) in C#


screenshot

// 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

Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 0 comments