/* Sliding Tile Game Copyright (c) 1997 by Ming-Yee Iu This is a quick imitation of one of those simple 15 piece sliding tile games. It is composed of about 5 objects and makes a half-hearted attempt at MVC. OBJECTS: Observer - an interface that allows the GameBoard and TimerNotifier to callback to the TileGame when the TileGame should redraw the screen. TileGame - controls the UI GameBoard - holds the gameboard data and game logic TimerNotifier - triggers a callback every second. MAIN INTERACTIONS: The TileGame extends applet and sets up the screen. It uses the various Java layout tools to layout the UI. The various tiles of the 15 sliding tile game are made up of buttons. Then TileGame starts the game (i.e. waits for events to occur). According to the MVC model, the TileGame acts as the view and controller. Although the view and controller are in the same object, the split between them is clean. When a button press event is received by the action method of TileGame (a controller responsibility), the TileGame calls methods in GameBoard to shift the tiles around (a model responsibility). Shifting around the tiles causes the Gameboard to call back to the TileGame via the NotificationCallback method of the Observer method to tell it to update the buttons to reflect the new state. The TileGame then updates the button with new data from the GameBoard (a view responsibility). The TimerNotifier is an object that creates its own thread to count the time. It isn't very accurate because it uses the thread-control routines to do the timing. Every second, the TimerNotifier uses the NotificationCallback method of TileGame to notify the TileGame of the new time. TileGame then updates the display. */ import java.awt.*; import java.applet.Applet; import java.lang.Object; import java.lang.Math; import java.lang.Thread; /*This Observer interface provides a means for an object to be notified of changes of state in another object. This Observer interface is a stripped down version of the observer pattern described in the "Gang of Four" Design Patterns book. I actually haven't gotten around to reading the book yet, but by making references to it, I can make people think that I know a lot more about design than I actually do (Smalltalk, Objects, and Design by Chammond Liu has a very readable chapter which I did read that summarizes some of the important patterns). Anyway, the interface is used when the TimerNotifier and GameBoard want to notify the "view" portion of the TileGame object that the UI needs updating.*/ interface Observer { void NotificationCallback(); } /*This class maintains the actual on-screen view of the game.*/ public class TileGame extends Applet implements Observer { GameBoard Puzzle; //All the puzzle logic is in here. TimerNotifier Timer = new TimerNotifier(this); //UI stuff. Panel TilePanel = new Panel(); Button TileButtons[][] = new Button[4][4]; Button ShuffleButton = new Button("Shuffle"); Label TimerLabel = new Label("Time: 0"); /*Set to true at the beginning when the gameboard is in a sort of "limbo" state when the user hasn't clicked on the shuffle button to start playing the game.*/ boolean StartOfGame = true; //Standard applet initialization / UI setup and stuff. public void init() { resize(220, 250); setLayout(new BorderLayout() ); Puzzle = new GameBoard(this); //Create the main tile panel with the 15/16 buttons on it. TilePanel.setLayout( new GridLayout(4,4) ); for (int x=0; x<4; x++) for (int y=0; y<4; y++) { TileButtons[x][y] = new Button(); TilePanel.add(TileButtons[x][y]); } /*add the panel to the applet window with a timer above it and a reshuffle button under it.*/ add("Center", TilePanel); add("North", TimerLabel); add("South", ShuffleButton); } //Applet is activated public void start() { Timer.Start(); UpdateWindow(); } //Applet is deactivated (loses focus, not on the screen?) public void stop() { Timer.Stop(); } //This is the callback from the model for when the gameboard changes //so that the on-screen gameboard can be redrawn. public void NotificationCallback() { UpdateWindow(); } //Redraw the window. void UpdateWindow() { //Updates the buttons etc. int n=0; for (int x=0; x<4; x++) for (int y=0; y<4; y++, n++) { /* UI updates on some machines are expensive operations (it's so slow that the user can see the UI updating). In order to ensure that the user doesn't have to unnecesarily see the UI flicker as it updates, the buttons are checked first to see if the data in them have changed. */ if (TileButtons[x][y].getLabel() != String.valueOf(Puzzle.GetTile(x, y))) { if (x == Puzzle.GetEmptyTileX() && y == Puzzle.GetEmptyTileY()) { TileButtons[x][y].hide(); TileButtons[x][y].setLabel(String.valueOf(Puzzle.GetTile(x, y))); } else { TileButtons[x][y].setLabel(String.valueOf(Puzzle.GetTile(x, y))); TileButtons[x][y].show(); } } } //Choose an appropriate message for the top. if (Puzzle.IsComplete() && !StartOfGame) { Timer.Stop(); //hack to make sure the timer doesn't keep ticking after the screen has been updated. Ideally it should be in action, but then a timer tick might occur at the wrong moment causing the timer to advance even after the game has been won. TimerLabel.setText("Puzzle Completed in ".concat(String.valueOf(Timer.GetTime())).concat(" seconds!")); } else if (Puzzle.IsComplete()) TimerLabel.setText("Press the Shuffle Button"); else TimerLabel.setText( "Time: ".concat(String.valueOf(Timer.GetTime())) ); } //This callback is usually called when a button is pressed. public boolean action(Event evt, Object what) { if (evt.target == ShuffleButton) { //Shuffle button pressed Puzzle.Shuffle(); StartOfGame = false; Timer.Reset(); Timer.Start(); return true; } else { for (int x=0; x<4; x++) for (int y=0; y<4; y++) if (evt.target == TileButtons[x][y]) { //If one of the tile buttons was pressed then shift the tiles if (Puzzle.IsComplete() == false) { Puzzle.ShiftTiles(x, y); //only let the user push the tiles if the puzzle is not complete return true; } } } return false; } } /*This class contains the code to handle the game board logic and model.*/ class GameBoard extends Object { //Holds the data for the tiles--i.e. which number is in which tile. //Tile in the upper right is 3,0. Observer GameBoardView; int Tiles[][] = new int[4][4]; int EmptyTileX, EmptyTileY; //the position of the empty tile. GameBoard(Observer View) { GameBoardView = View; //set all the tiles to be in the "home" position ResetTiles(); } //Used to notify the view to update the UI when the gameboard is changed. void NotifyViews() { GameBoardView.NotificationCallback(); } //Set all the tiles to a "home" position. void ResetTiles() { int n = 1; for( int x=0; x<4; x++) for( int y=0; y<4; y++) { Tiles[x][y] = n; n = (n + 1) % 16; } EmptyTileX = EmptyTileY = 3; } //Shifts all the tiles around and nothing else. private void BasicShiftTiles(int x, int y) { //assumes 0 <= x <= 3 and 0 <= y <= 3 //Given a tile position, this method will shift all the tiles from the //given tile position toward the empty tile. if (y == EmptyTileY) { while (x < EmptyTileX) { //swap the empty tile with the tile to the left of it. Tiles[EmptyTileX][EmptyTileY] = Tiles[EmptyTileX-1][EmptyTileY]; Tiles[EmptyTileX-1][EmptyTileY] = 0; EmptyTileX--; } while (x > EmptyTileX) { //swap the empty tile with the tile to the right of it. Tiles[EmptyTileX][EmptyTileY] = Tiles[EmptyTileX+1][EmptyTileY]; Tiles[EmptyTileX+1][EmptyTileY] = 0; EmptyTileX++; } } if (x == EmptyTileX) { while (y < EmptyTileY) { //swap the empty tile with the tile to the left of it. Tiles[EmptyTileX][EmptyTileY] = Tiles[EmptyTileX][EmptyTileY-1]; Tiles[EmptyTileX][EmptyTileY-1] = 0; EmptyTileY--; } while (y > EmptyTileY) { //swap the empty tile with the tile to the right of it. Tiles[EmptyTileX][EmptyTileY] = Tiles[EmptyTileX][EmptyTileY+1]; Tiles[EmptyTileX][EmptyTileY+1] = 0; EmptyTileY++; } } } //Shifts the tiles around and notifies the views of the change. public void ShiftTiles(int x, int y) { //assumes 0 <= x <= 3 and 0 <= y <= 3 //Given a tile position, this method will shift all the tiles from the //given tile position toward the empty tile. BasicShiftTiles(x, y); NotifyViews(); } //Returns the value of a certain tile. public int GetTile(int x, int y) { return Tiles[x][y]; } //Returns the x position of the empty tile. public int GetEmptyTileX() { return EmptyTileX; } //Returns the y position of the empty tile. public int GetEmptyTileY() { return EmptyTileY; } //Returns true when the puzzle is complete. public boolean IsComplete() { boolean puzzleComplete = true; int n=1; for (int x=0; x<4; x++) for (int y=0; y<4; y++, n++) if (n<16) puzzleComplete &= Tiles[x][y] == n; return puzzleComplete; } public void Shuffle() { /* Shifts the tiles around 1000 times. A strange way of getting random numbers * is used because depending on how rounding is implemented, the number 3 * might otherwise never be generated.*/ int x, y; for (int n=1000; n > 0; n--) BasicShiftTiles((int)(Math.random() * 9) % 4, (int)(Math.random() * 9) % 4); NotifyViews(); } } //This class will call a notification callback every second. //It does get screwed up when repeatedly started and stopped (e.g. when the //window that uses it gains and loses focus) class TimerNotifier extends Object implements Runnable { int Time=0; //Elapsed time. Observer Listener; //Observer that will be notified on each second tick. Thread TimerThread; //The thread used to count the time. boolean stop = false; //Set to true when the timer is stopped. public TimerNotifier (Observer ObjectToBeNotified) { Listener = ObjectToBeNotified; //Start a thread to do timing in. TimerThread = new Thread(this); TimerThread.start(); } protected void finalize() { /*I'm not too sure how the thread control functions work, so to ensure that the observer isn't notified of timer ticks when the timer is dead, this stop variable is used to stop the timer thread from sending notification callbacks.*/ stop = true; } //Self-explanatory. public void Stop() { TimerThread.suspend(); } //Self-explanatory. public void Start() { TimerThread.resume(); } //Self-explanatory. public int GetTime() { return Time; } //Self-explanatory. public void Reset() { Time = 0; } //Called when the timer thread is started up. This is where the timing is actually done. public void run() { while (stop == false) { try { TimerThread.sleep(1000); //sleep for a second } catch (InterruptedException e) {}; Time++; Listener.NotificationCallback(); } } }