CCPS 109 Programming Project: Sokoban

The programming project for the course CCPS 109 is to implement Sokoban, the Japanese modern classic puzzle game. The rules are very simple yet they generate amazing complexity for the player who needs to use lateral thinking to solve these puzzles. For those interested of this game and its colourful history, or for the clarification of its rules, see the Wikipedia Sokoban page.

The instructor provides the level data files and the code needed to read them, and it is your job to implement the actual game as one class Sokoban that is a Swing component with a main method that opens a window that contains one instance of this component.

Rules of Sokoban

Each level of Sokoban is a two-dimensional “warehouse” room seen from above. The room is a grid of tiles, each of which is one of the following:

The purpose of the game is to push each box onto some goal tile, of which there are at least as many on each level as there are boxes. The difficulty is that the player, a warehouse keeper, can only push a box, but never pull a box. Furthermore, the player has the strength to push only one box at the time on either an empty space or a goal tile behind the box. In the game, this restriction makes many seemingly innocent moves irreversible: even one misstep can be fatal so that the level can no longer be completed.

Your game must allow the player to move around the level with the cursor keys. Furthermore, to help my testing and debugging, I require that pressing 'N' will immediately move the game to the next level in the game, and pressing 'R' will restart the current level from its initial state.

LevelReader.java

Since reading and writing data from a file will be covered in CCPS 209, and we should try to keep the size of this project manageable anyway, the instructor provides you with two classes SokoTile.java and LevelReader.java (as part of the 109.jar archive), along with the level data file m1.txt. You should not modify these two classes in any way.

SokoTile is an enumeration that defines the seven possible values that each tile can have, as listed above. LevelReader contains the methods that you need in your game to set up the contents of each level. These methods are

Note that this class does not contain any methods for modifying the contents of each level, but merely provides its initial configuration. Therefore your Sokoban class must maintain the state of the current level in a 2D array field of its own.

Note also that in the coordinate system used in these methods, the x-coordinate is horizontal (column), and the y-coordinate is vertical (row). It doesn't matter if you switch these two, as long as you do so consistently in all of your drawing and movement methods.

Note also that none of the above methods do any kind of error checking to their parameter. It is your responsibility to call these methods so that the parameter values are legal. However, your code can implicitly assume that each level is completely surrounded by solid walls, so there is no chance of the player or the box that is being going out of the array bounds. This way, you don’t need to write your code to check for that possibility.

Roadmap for your Sokoban.java

To implement your game as the class Sokoban, you don't need to follow the roadmap that I have sketched below. However, it will make things a lot easier if you do. Make sure that each stage of the roadmap is complete and works correctly before you even try to move on to the next stage. It is much easier to fix bugs as soon as you create them, and only build more code on top of existing code that is known to be correct, since any problems are then known to be in the newly added code and much easier to find.

  1. Write a field private LevelReader lr = new LevelReader(); in your Sokoban class. Write a constructor Sokoban(String fileName) that calls the readLevels method of the lr object.
  2. Write a method void initLevel(int level) that asks the lr object for the width and height of the current level. The method then sets up a 2D array of that size (this array must be a field instead of just a local variable, since it is needed in the methods below), and then fills it tile by tile by asking the level reader lr for the contents of each tile, and finally calls repaint.
  3. Write the method void paintComponent(Graphics g) that displays the contents of the current level, stored in the 2D array field of the previous method. You can initially just draw each type of tile as a rectangle using a different colour for each type of tile, and later when you get the game working, put in some better graphics, if you have time.
  4. Write a KeyListener nested class to your class. Make it work so that the 'R' and 'N' keys work as required, so that you can look at other levels and use them to test that your paintComponent method.
  5. Now, it’s time to move. Make you key listener work so that whenever some cursor key is pressed, the method loops through the room to find the coordinates of the player. When the player is found, move the player to the direction determined by which cursor key was pressed, if that location is empty or a goal tile. At this point, the player doesn't need to be able to push boxes yet, but just move around over the empty space and goals. Make sure that when the player moves away from top of a goal tile, the tile becomes a goal instead of empty.
  6. If you haven’t done so already, go back to redesign the previous method so that it doesn't consist of four identical branches of code for the four different directions, but you have only one branch that is parameterized with movement offsets dx and dy. (For example, going up is represented by having dx=0 and dy=-1, and the other directions are handled similarly.)
  7. Seriously, do that. The next step will be a lot easier after you simplify and streamline your code so that instead of having to do the error prone dance of writing and maintaining four branches, there is only one branch.
  8. Modify the key listener so that if the player tries to move to a location that contains a box, check if the space behind the box is empty or a goal tile, and if so, move the box there, and move the player to the location where the box used to be. Again, be careful to update each tile so that neither the player nor the box erases a goal on the floor as it moves over it.
  9. Write a method private boolean checkWin() that loops through the room and returns false if there exists even one box that is not on some goal, and true otherwise. Call this method every time you move a box, and if it returns true, move on to the next level.

The single most important thing about this project is the clarity and brevity in the movement logic. Try to eliminate as much redundancy as you can from the key listener method, but after each modification, be careful to ensure that it still works correctly! As a general rule about code smell, whenever you are doing the same thing twice, or copypasting your own code within the same project, you are doing something wrong, or at least not cleanly enough. You should never in any program have identical branches of code that differ only by some one little thing. The moment you realize that you have created such branches, think of how you could refactor these branches into one branch or a helper method.