/// <summary>Runs the MoveFinder using the specified options.</summary> /// <param name="level">The Sokoban level under consideration.</param> /// <param name="stopIfFourSides">If not null, runs a MoveFinder only so far as /// necessary to ascertain that all four cells adjacent to the specified cell /// are reachable.</param> /// <param name="ignorePieces">If true, only walls are considered obstacles.</param> private void run(SokobanLevel level, Point?stopIfFourSides, bool ignorePieces) { // Contains the set of cells yet to be considered Queue <Point> queue = new Queue <Point>(); _level = level; _pathLength = new int[_level.Width * _level.Height]; _predecessor = new Point[_level.Width * _level.Height]; queue.Enqueue(_level.SokobanPos); _pathLength[_level.SokobanPos.Y * _level.Width + _level.SokobanPos.X] = 1; // Breadth-first search: extract an item from the queue, process it, and add // the newly-discovered items to the queue (Examine() does that). while (queue.Count > 0) { Point pivot = queue.Dequeue(); examine(new Point(pivot.X, pivot.Y - 1), pivot, ignorePieces, queue); examine(new Point(pivot.X - 1, pivot.Y), pivot, ignorePieces, queue); examine(new Point(pivot.X + 1, pivot.Y), pivot, ignorePieces, queue); examine(new Point(pivot.X, pivot.Y + 1), pivot, ignorePieces, queue); if (stopIfFourSides != null && _pathLength[stopIfFourSides.Value.Y * _level.Width + stopIfFourSides.Value.X - _level.Width] > 0 && _pathLength[stopIfFourSides.Value.Y * _level.Width + stopIfFourSides.Value.X + _level.Width] > 0 && _pathLength[stopIfFourSides.Value.Y * _level.Width + stopIfFourSides.Value.X - 1] > 0 && _pathLength[stopIfFourSides.Value.Y * _level.Width + stopIfFourSides.Value.X + 1] > 0) { queue.Clear(); } } }
/// <summary> /// Initialisation. Called by each of the overloaded constructors to perform the /// common initialisation work. /// </summary> /// <param name="level">The SokobanLevel that is to be rendered.</param> /// <param name="clientWidth">The width (in pixels) of the client area into which the level is to be rendered.</param> /// <param name="clientHeight">The height (in pixels) of the client area into which the level is to be rendered.</param> private void initialize(SokobanLevel level, int clientWidth, int clientHeight) { _clientWidth = clientWidth; _clientHeight = clientHeight; _level = level; int idealWidth = _clientHeight * level.Width / level.Height; _cellWidth = _cellHeight = _clientWidth > idealWidth ? _clientHeight / level.Height : _clientWidth / level.Width; _originX = (int)(_clientWidth / 2f - _cellWidth * level.Width / 2f); _originY = (int)(_clientHeight / 2f - _cellHeight * level.Height / 2f); }
/// <summary> /// Sets the form's contents in such a way that the specified highscores are displayed. /// </summary> /// <param name="highscores">A dictionary mapping from player name to highscores.</param> /// <param name="level">The level whose highscores are being displayed.</param> public void SetContents(Dictionary <string, Highscore> highscores, SokobanLevel level) { pnlHighscores.ColumnCount = 3; ctLevelPicture.ClientSize = new Size(ctLevelPicture.ClientSize.Width, (int)(ctLevelPicture.ClientSize.Width * level.Height / level.Width)); _level = level.Clone(); _level.EnsureSpace(1); List <string> playerNames = new List <string>(); foreach (string s in highscores.Keys) { Highscore h = highscores[s]; int i = 0; while ((i < playerNames.Count) && (h.CompareTo(highscores[playerNames[i]]) < 0)) { i++; } playerNames.Insert(i, s); } for (int i = 0; i < playerNames.Count; i++) { Label lblPlayerName = new Label(); lblPlayerName.Font = new Font(Font, FontStyle.Bold); lblPlayerName.Text = playerNames[i]; lblPlayerName.AutoSize = true; lblPlayerName.Margin = new Padding(5); pnlHighscores.Controls.Add(lblPlayerName, 1, i); Highscore h = highscores[playerNames[i]]; Label lblScore = new Label(); lblScore.Text = Program.Tr.Highscores.Highscores.Fmt(Program.Tr.Language.GetNumberSystem(), h.BestPushScore.Moves, h.BestPushScore.Pushes); lblScore.AutoSize = true; lblScore.Margin = new Padding(5); pnlHighscores.Controls.Add(lblScore, 2, i); } pnlHighscores.Controls.Add(btnOK, 2, playerNames.Count); pnlHighscores.Controls.Add(ctLevelPicture, 0, 0); pnlHighscores.SetRowSpan(ctLevelPicture, playerNames.Count + 1); while (pnlHighscores.ColumnStyles.Count < 3) { pnlHighscores.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); } while (pnlHighscores.RowStyles.Count < playerNames.Count + 1) { pnlHighscores.RowStyles.Add(new RowStyle(SizeType.AutoSize)); } }
private void saveLevel(object sender, EventArgs e) { SokobanLevel level = ctMainArea.Level.Clone(); level.EnsureSpace(0); // LevelList.EditAccept() will trigger the LevelActivating event, which in // turn will ask the user if they want to discard their changes to the level. // Since we don't want this, we have to set the Modified flag for the MainArea // to false before calling LevelList.EditAccept(). ctMainArea.Modified = false; lstLevels.EditAccept(level); }
/// <summary> /// Renders the specified level and returns the finished image. Renderings are /// cached for efficiency. /// </summary> /// <param name="level">The SokobanLevel to be rendered.</param> /// <param name="width">Width of the area to render into.</param> /// <param name="height">Height of the area to render into.</param> private Image getRendering(SokobanLevel level, int width, int height) { if (_cachedRenderings.ContainsKey(level)) { return((Image)_cachedRenderings[level]); } Image rendering = new Bitmap(width, height); using (var graphics = Graphics.FromImage(rendering)) new Renderer(level, width, height, new SolidBrush(Color.Transparent)).Render(graphics); _cachedRenderings[level] = rendering; return(rendering); }
/// <summary> /// Accept the specified level as the new version of the level being edited. /// Turns off the Edit mode and starts playing the new level (if it is valid). /// </summary> public void EditAccept(SokobanLevel level) { // Update the level Items[_activeLevelIndex.Value] = level; _modified = true; // Play this level if (level.Validity == SokobanLevelStatus.Valid) { playingIndex = _activeLevelIndex; SelectedIndex = _activeLevelIndex.Value; // select this level in the box - probably desirable? } else { playingIndex = null; } }
/// <summary> /// Occurs when the list box needs to determine the size (height) of an item. /// Levels are measured in such a way that the height of the item is to its /// width as the height of the level is to its width. The "currently playing" /// or "currently editing" and the "solved" messages are taken into account. /// </summary> private void measureItem(object sender, MeasureItemEventArgs e) { if (e.Index < 0 || e.Index >= Items.Count || DesignMode) { return; } if (Items[e.Index] is SokobanLevel) { SokobanLevel level = (SokobanLevel)Items[e.Index]; e.ItemHeight = (ClientSize.Width - 10) * level.Height / level.Width + 10; if (e.Index == playingIndex) // also covers "Just Solved" { e.ItemHeight += (int)e.Graphics.MeasureString(Program.Tr.LevelList_CurrentlyPlaying, Font).Height + 5; } else if (e.Index == editingIndex) { e.ItemHeight += (int)e.Graphics.MeasureString(Program.Tr.LevelList_CurrentlyEditing, Font).Height + 5; } if (Program.Settings.IsSolved(Items[e.Index].ToString())) { e.ItemHeight += (int)e.Graphics.MeasureString(Program.Tr.LevelList_LevelSolved, Font).Height + 5; } } else if (Items[e.Index] is string && (Items[e.Index] as string).Length == 0) { e.ItemHeight = (int)e.Graphics.MeasureString("Mg", Font).Height + 10; } else if (Items[e.Index] is string) { e.ItemHeight = (int)e.Graphics.MeasureString((string)Items[e.Index], Font).Height + 10; } else { e.ItemHeight = (int)e.Graphics.MeasureString(Items[e.Index].ToString(), Font).Height + 10; } if (e.ItemHeight > 255) { e.ItemHeight = 255; } }
/// <summary> /// Constructs a Renderer object. /// </summary> /// <param name="level">The SokobanLevel that is to be rendered.</param> /// <param name="clientWidth">The width (in pixels) of the client area into which the level is to be rendered.</param> /// <param name="clientHeight">The height (in pixels) of the client area into which the level is to be rendered.</param> /// <param name="backgroundBrush">The Brush used to fill the background of the level.</param> public Renderer(SokobanLevel level, int clientWidth, int clientHeight, Brush backgroundBrush) { _backgroundBrush = backgroundBrush; initialize(level, clientWidth, clientHeight); }
/// <summary>Runs a MoveFinder using the specified mode of operation.</summary> /// <param name="level">The Sokoban level under consideration.</param> /// <param name="specialOption">A set of <see cref="MoveFinderOption"/> flags /// specifying the behaviour of the MoveFinder.</param> public MoveFinder(SokobanLevel level, MoveFinderOption specialOption) { run(level, null, specialOption == MoveFinderOption.IgnorePieces); }
/// <summary> /// Runs a MoveFinder only so far as necessary to ascertain that all four cells /// adjacent to the specified cell are reachable. /// </summary> /// <param name="level">The Sokoban level under consideration.</param> /// <param name="stopIfFourSides">The cell whose four adjacent cells are under /// consideration.</param> public MoveFinder(SokobanLevel level, Point stopIfFourSides) { run(level, stopIfFourSides, false); }
/// <summary>Runs a normal MoveFinder.</summary> /// <param name="level">The Sokoban level under consideration.</param> public MoveFinder(SokobanLevel level) { run(level, null, false); }
/// <summary> /// Main constructor. /// </summary> public MoveFinderOutline(SokobanLevel level) : base(level, MoveFinderOption.IgnorePieces) { // This space intentionally left blank // (the base constructor calls run()) }
/// <summary> /// Loads a level file from the level list. /// </summary> public void LoadLevelPack(string path) { // Will contain the list of levels and comments that we have loaded. // (We only want to add them to the listbox at the end in case we // throw an exception, because then we want to keep the old file.) List <object> loadedItems = new List <object>(); // Class to read from the text file StreamReader sr = new StreamReader(path, Encoding.UTF8); // State we're in (Empty, Comment or Level) levelReaderState state = levelReaderState.Empty; // Line last read String line; // Current comment (gets appended to until we reach the end of the comment) String comment = ""; // Current level (gets appended to until we reach the end of the level) String levelEncoded = ""; do { line = sr.ReadLine(); // If this line begins with a semicolon (';'), it's a comment. // Otherwise, it is considered part of the level and must // contain only the characters #@$.*+ or space. if (line != null && line.Length > 0 && line[0] != ';') { // check for invalid characters for (int i = 0; i < line.Length; i++) { if (line[i] != ' ' && line[i] != '#' && line[i] != '@' && line[i] != '$' && line[i] != '.' && line[i] != '*' && line[i] != '+') { throw new InvalidLevelException(); } } } // Decide whether this line belongs to a level or comment levelReaderState newState = (line == null || line.Length == 0) ? levelReaderState.Empty : line[0] == ';' ? levelReaderState.Comment : levelReaderState.Level; // If we are switching from level to comment or vice versa, // or reaching the end of the file, add the level or comment // to the level list and empty the relevant variable if (newState != state && state == levelReaderState.Comment) { loadedItems.Add(comment); comment = ""; } else if (newState != state && state == levelReaderState.Level) { SokobanLevel newLevel = new SokobanLevel(levelEncoded); newLevel.EnsureSpace(0); loadedItems.Add(newLevel); levelEncoded = ""; } // Append the line we just read to the relevant variable if (newState == levelReaderState.Comment) { comment += line.Substring(1) + "\n"; } else if (newState == levelReaderState.Level) { levelEncoded += line + "\n"; } // Update the state state = newState; } while (line != null); _activeLevelIndex = null; BeginUpdate(); Items.Clear(); foreach (object item in loadedItems) { Items.Add(item); } EndUpdate(); sr.Close(); _modified = false; Program.Settings.LevelFilename = path; }
/// <summary>Main constructor. Runs the push finder.</summary> /// <param name="level">The Sokoban level under consideration.</param> /// <param name="selectedPiece">The co-ordinates of the currently selected piece /// (which is the source node for our algorithm).</param> /// <param name="moveFinder">The MoveFinder for the initial level situation.</param> public PushFinder(SokobanLevel level, Point selectedPiece, MoveFinder moveFinder) { _level = level; specialHeap priorityQueue = new specialHeap(this); Point origPiecePos = selectedPiece; Point origSokPos = _level.SokobanPos; int arraySize = 4 * level.Width * level.Height; _pushLength = new int[arraySize]; _moveLength = new int[arraySize]; _predecessor = new int[arraySize]; bool[] extracted = new bool[arraySize]; _path = new Point[arraySize][][]; _moveFinder = moveFinder; // In Dijkstra's algorithm, you usually start with a priority queue containing only // the source node. Then the first iteration extracts that source node from the // priority queue again, relaxes all its edges, and inserts the newly-discovered // nodes. We will run this "first iteration" manually because our source node is // special and not like all the others. We will end up inserting up to four nodes // into the priority queue, and then we will proceed with the iterative algorithm. addIfValid(nodeIndex(origPiecePos, 1), new Point(origPiecePos.X, origPiecePos.Y - 1), priorityQueue); addIfValid(nodeIndex(origPiecePos, 2), new Point(origPiecePos.X - 1, origPiecePos.Y), priorityQueue); addIfValid(nodeIndex(origPiecePos, 3), new Point(origPiecePos.X + 1, origPiecePos.Y), priorityQueue); addIfValid(nodeIndex(origPiecePos, 4), new Point(origPiecePos.X, origPiecePos.Y + 1), priorityQueue); // Dijkstra's algorithm: We extract a node from our priority queue and relax // all its outgoing edges. However, we might not yet know the length of those // edges; we might have to invoke MoveFinder to determine them. We also don't // have all nodes in the priority queue; we are adding them as we discover them. while (!priorityQueue.Empty) { // Extract the next item and filter out duplicates. // Since reheapification is only possible when adding or extracting items, // not when items change their value, we simply add nodes to the priority // queue every time a node changes its value. This means some nodes will // have several copies in the priority queue, and we are only interested // in each node the first time it is extracted. int node = priorityQueue.Extract(); if (extracted[node]) { continue; } extracted[node] = true; // The item we have extracted represents a node in our graph // that we want to run Dijkstra's algorithm on. That node, in // turn, represents a situation in which: // (1) the piece we are moving is at the position given by NodeToPos(node); Point position = nodeToPos(node); // (2) the Sokoban is located: // 1 = above the piece, 2 = left, 3 = right, 4 = below // as given by NodeToDir(node). int dirFrom = nodeToDir(node); // For the duration of this one iteration of the algorithm, we will // manipulate the level by placing the piece and the Sokoban in the // right place. We will undo this change at the end of the iteration. _level.MovePiece(origPiecePos, position); Point newSokPos = posDirToPos(position, dirFrom); _level.SetSokobanPos(newSokPos); // The node we're at has up to four possible outgoing edges. // These are: // (1) up to three of the following four: // (a) moving the Sokoban to above the piece // (b) moving the Sokoban to left of the piece // (c) moving the Sokoban to right of the piece // (d) moving the Sokoban to below the piece // (2) pushing the piece forward one cell. // We will first examine (1)(a-d). To do that, we need to find out // which of the other cells adjacent to the piece the Sokoban can move // to. We do this by running a MoveFinder. MoveFinder mf = new MoveFinder(_level, position); _path[node] = new Point[5][]; // For any of these cells, mf.Path() will return null if you can't walk to it if (dirFrom != 1) { _path[node][1] = mf.Path(new Point(position.X, position.Y - 1)); } if (dirFrom != 2) { _path[node][2] = mf.Path(new Point(position.X - 1, position.Y)); } if (dirFrom != 3) { _path[node][3] = mf.Path(new Point(position.X + 1, position.Y)); } if (dirFrom != 4) { _path[node][4] = mf.Path(new Point(position.X, position.Y + 1)); } if (_path[node][1] != null) { relaxEdge(node, nodeIndex(position, 1), 0, _path[node][1].Length, priorityQueue); } if (_path[node][2] != null) { relaxEdge(node, nodeIndex(position, 2), 0, _path[node][2].Length, priorityQueue); } if (_path[node][3] != null) { relaxEdge(node, nodeIndex(position, 3), 0, _path[node][3].Length, priorityQueue); } if (_path[node][4] != null) { relaxEdge(node, nodeIndex(position, 4), 0, _path[node][4].Length, priorityQueue); } // Finally, consider possibility (2): pushing the piece. Point pushTo = posDirToPos(position, oppositeDir(dirFrom)); if (_level.IsFree(pushTo)) { relaxEdge(node, nodeIndex(pushTo, dirFrom), 1, 1, priorityQueue); } // Before moving on to the next iteration, restore the level as it was. _level.MovePiece(position, origPiecePos); _level.SetSokobanPos(origSokPos); } }
/// <summary> /// Constructs a Renderer object. /// </summary> /// <param name="level">The SokobanLevel that is to be rendered.</param> /// <param name="clientSize">The size (in pixels) of the client area into which the level is to be rendered.</param> /// <param name="backgroundBrush">The Brush used to fill the background of the level.</param> public Renderer(SokobanLevel level, Size clientSize, Brush backgroundBrush) { _backgroundBrush = backgroundBrush; initialize(level, clientSize.Width, clientSize.Height); }
/// <summary> /// Constructs a Renderer object. /// </summary> /// <param name="level">The SokobanLevel that is to be rendered.</param> /// <param name="clientWidth">The width (in pixels) of the client area into which the level is to be rendered.</param> /// <param name="clientHeight">The height (in pixels) of the client area into which the level is to be rendered.</param> public Renderer(SokobanLevel level, int clientWidth, int clientHeight) { initialize(level, clientWidth, clientHeight); }
/// <summary> /// Constructs a Renderer object. /// </summary> /// <param name="level">The SokobanLevel that is to be rendered.</param> /// <param name="clientSize">The size (in pixels) of the client area into which the level is to be rendered.</param> public Renderer(SokobanLevel level, Size clientSize) { initialize(level, clientSize.Width, clientSize.Height); }