Click here to Skip to main content
15,886,026 members
Articles / Desktop Programming / WPF

WPF Alien Sokoban

Rate me:
Please Sign up or sign in to vote.
4.88/5 (44 votes)
16 Jun 2008BSD9 min read 125.4K   3.4K   78  
A fun implementation of the game Sokoban, written to showcase some features of WPF, C# 3.0, Expression Design, and Visual Studio 2008.
/*	<File>
		<Author Name="Daniel Vaughan" Email="dbvaughan NOSPAM at NOSPAM gmail NOSPAM dot NOSPAM com"/>
		<CreationDate>2007/10/29 18:09</CreationDate>
		<LastSubmissionDate>$Date: $</LastSubmissionDate>
		<Version>$Revision: $</Version>
		<License>
Copyright © 2007, Daniel Vaughan
All rights reserved.
http://www.orpius.com

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

- Neither Daniel Vaughan, nor the names of its
contributors may be used to endorse or promote products
derived from this software without specific prior written 
permission. 

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
		</License>
	</File>
*/

using System;
using System.Collections.Generic;
using System.IO;

namespace Orpius.Sokoban
{
	/// <summary>
	/// Represents a single stage within a game.
	/// A level instance is able to load itself
	/// from a map resource.
	/// </summary>
	public class Level
	{
		Cell[][] cells;
		List<GoalCell> goals = new List<GoalCell>();

		/// <summary>
		/// Gets the level number.
		/// </summary>
		/// <value>The level number.</value>
		public int LevelNumber
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets or sets the game that this level is located.
		/// </summary>
		/// <value>The game that this level is located.</value>
		protected Game Game
		{
			get;
			set;
		}

		/// <summary>
		/// Gets the single <see cref="Actor"/> instance
		/// located on each level.
		/// </summary>
		/// <value>The actor. (The user moveable guy) </value>
		public Actor Actor
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets the <see cref="Orpius.Sokoban.Cell"/> with the specified row number
		/// and column number.
		/// </summary>
		/// <value>The cell at the specified row number and column number.</value>
		public Cell this[int rowNumber, int columnNumber]
		{
			get
			{
				return cells[rowNumber][columnNumber];
			}
		}

		/// <summary>
		/// Gets the <see cref="Orpius.Sokoban.Cell"/> with the specified location.
		/// </summary>
		/// <value>The cell at the specified location.</value>
		public Cell this[Location location]
		{
			get
			{
				if (InBounds(location))
				{
					return this[location.RowNumber, location.ColumnNumber];
				}
				return null;
			}
		}

		/// <summary>
		/// Gets the number of rows in the level.
		/// </summary>
		/// <value>The number of rows in the level.</value>
		public int RowCount
		{
			get
			{
				return cells != null ? cells.Length : 0;
			}
		}

		/// <summary>
		/// Gets the number of columns in the level.
		/// </summary>
		/// <value>The number of columns in the level.</value>
		public int ColumnCount
		{
			get
			{
				return cells != null && cells.Length > 0 ? cells[0].Length : 0;
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="Level"/> class.
		/// </summary>
		/// <param name="game">The game that the level belongs.</param>
		/// <param name="levelNumber">The level number for this level.</param>
		public Level(Game game, int levelNumber)
		{
			Game = game;
			LevelNumber = levelNumber;
		}

		/// <summary>
		/// Loads the level data from the specified map stream.
		/// </summary>
		/// <param name="mapStream">The map stream to load the level.</param>
		public void Load(TextReader mapStream)
		{
			if (mapStream == null)
			{
				throw new ArgumentNullException("mapStream");
			}

			List<List<Cell>> rows = new List<List<Cell>>();

			string gridRowText;
			int rowCount = 0;
			while ((gridRowText = mapStream.ReadLine()) != null && gridRowText.Trim() != string.Empty)
			{
				rows.Add(BuildCells(gridRowText, rowCount++));
			}

			cells = new Cell[rowCount][];
			for (int i = 0; i < rowCount; i++)
			{
				cells[i] = rows[i].ToArray();
			}
		}

		List<Cell> BuildCells(string rowText, int rowNumber)
		{
			List<Cell> row = new List<Cell>(rowText.Length);
			
			int columnNumber = 0;
			
			foreach (char c in rowText)
			{
				Location location = new Location(rowNumber, columnNumber++);
				switch (c)
				{
					case '#': /* Wall. */
						row.Add(new WallCell(location, this));
						break;
					case ' ': /* Empty. */
						row.Add(new FloorCell(location, this));
						break;
					case '$': /* Treasure in a square. */
						row.Add(new FloorCell(location, this, new Treasure(location, this)));
						break;
					case '*': /* Treasure in a Goal. */
						GoalCell goalCellWithTreasure = new GoalCell(location, this, new Treasure(location, this));
						goalCellWithTreasure.CompletedGoalChanged += new EventHandler(GoalCell_CompletedGoalChanged);
						goals.Add(goalCellWithTreasure);
						row.Add(goalCellWithTreasure);
						break;
					case '.': /* Goal. */
						GoalCell goalCell = new GoalCell(location, this);
						goalCell.CompletedGoalChanged += new EventHandler(GoalCell_CompletedGoalChanged);
						goals.Add(goalCell);
						row.Add(goalCell);
						break;
					case '@': /* Actor in a floor cell. */
						Actor actor = new Actor(location, this);
						row.Add(new FloorCell(location, this, actor));
						Actor = actor;
						break;
					case '!': /* Space. */
						row.Add(new SpaceCell(location, this));
						break;
					default:
						throw new FormatException("Invalid Level symbol found: " + c);
				}
			}
			return row;
		}

		void GoalCell_CompletedGoalChanged(object sender, EventArgs e)
		{
			foreach (GoalCell goal in goals)
			{
				if (!goal.HasTreasure)
				{
					return;
				}
			}
			OnLevelCompleted(EventArgs.Empty);
		}

		/// <summary>
		/// Tests whether the specified location is within 
		/// the Level grid.
		/// </summary>
		/// <param name="location">The location to test
		/// whether it is within the level grid.</param>
		/// <returns><code>true</code> if the location
		/// is within the <see cref="Level"/>; 
		/// <code>false</code> otherwise.</returns>
		public bool InBounds(Location location)
		{
			return (location.RowNumber >= 0
				&& location.RowNumber < RowCount
				&& location.ColumnNumber >= 0
				&& location.ColumnNumber < ColumnCount);
		}
		#region LevelCompleted event

		event EventHandler levelCompleted;

		/// <summary>
		/// Occurs when a level has been completed successfully.
		/// </summary>
		public event EventHandler LevelCompleted
		{
			add
			{
				levelCompleted += value;
			}
			remove
			{
				levelCompleted -= value;
			}
		}

		/// <summary>
		/// Raises the <see cref="E:LevelCompleted"/> event.
		/// </summary>
		/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		protected void OnLevelCompleted(EventArgs e)
		{
			if (levelCompleted != null)
			{
				levelCompleted(this, e);
			}
		}
		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Engineer
Switzerland Switzerland
Daniel is a former senior engineer in Technology and Research at the Office of the CTO at Microsoft, working on next generation systems.

Previously Daniel was a nine-time Microsoft MVP and co-founder of Outcoder, a Swiss software and consulting company.

Daniel is the author of Windows Phone 8 Unleashed and Windows Phone 7.5 Unleashed, both published by SAMS.

Daniel is the developer behind several acclaimed mobile apps including Surfy Browser for Android and Windows Phone. Daniel is the creator of a number of popular open-source projects, most notably Codon.

Would you like Daniel to bring value to your organisation? Please contact

Blog | Twitter


Xamarin Experts
Windows 10 Experts

Comments and Discussions