Introduction
In this assignment, you will use the model-view-controller design pattern together with the JavaFX UI library to design a complete, functioning GUI implementation of the single-player logic puzzle nonograms.
If you've never heard of nonograms before, I highly suggest starting by checking out the nonograms Wikipedia page. After you do that, try solving a few nonogram puzzles yourself to make sure you've got the hang of it.
The nonogram app that you will design will be very similar to the nonograms implementation on the website linked above. You are encouraged to be creative with regard to the visual design of your implementation. Feel free to theme your user interface, move the components around in different locations, add new features, or make other significant changes to the look and feel of your app. However, your implementation must have the following features at a bare minimum for full credit:
1. If the user left-clicks on a cell, that cell becomes shaded. But, if they right-click on a cell, that cell becomes eliminated---just like the reference implementation. There must be a visual difference between a shaded and an eliminated cell. A cell cannot be both shaded and eliminated. Cells must be clearly marked, so the user can tell which cell they are selecting.
2. If the user left-clicks on an already-shaded cell, that cell should return to a blank state. If the user right-clicks on an already-eliminated cell, that cell should return to a blank state.
3. After each move, your app must check to see if the user solved the puzzle. If so, the UI must visually update to let the user know that they completed the puzzle. The puzzle is "solved" if the shaded squares match the clues. Non-shaded squares do not need to be labeled "eliminated" in order to solve the puzzle.
4. The UI must include a clearly visible and labeled "reset" button that will clear all the cells in the board and let the user start over from a blank state.
5. The starter code contains a pre-coded library of 5 puzzles to solve. The UI must provide clearly visible and labeled buttons to go to the next puzzle, to go to the previous puzzle, and to jump to a random puzzle. The "previous" button should not cause an uncaught exception if the user accidentally presses it on the first puzzle. Similarly, the "next" button should not cause an uncaught exception if the user accidentally presses it on the last puzzle.
6. The library of puzzles is a List, which means the puzzles are indexed. Your app must clearly display the index of the active puzzle in the library and how many puzzles are in the library in total. The displayed index should start from one, not zero. In other words, the first puzzle in the List (at index 0) should be displayed as "puzzle 1 of 5" to the user.
7. Your app must support arbitrary-sized boards with different widths and heights. To demonstrate this functionality, the provided pre-coded library of puzzles includes puzzles of at least two different sizes.
Starter code
Once you're familiar with nonograms, take a look at the starter code defined in the repository. Three packages have been created to help you organize your code according to the model-view-controller pattern. In these packages, a few starter interfaces and classes have been defined to help you structure your app.
It is not strictly necessary to use all the provided starter interfaces. Most of them are just there to give your app some structure and help you plan your code. If you would rather come up with your own design, there are a few rules that your code must follow so that your code can be autograded correctly. First, you must provide a ModelImpl and CluesImpl class as specified below. Second, running the main() method of the provided Main class must launch your GUI (this is how the LAs will launch your app for manual grading). Other than these two requirements, you are free to safely ignore the rest of the provided interfaces.
Running the application
This assignment uses Maven as a build manager, and JavaFX as a GUI library. Since JavaFX is an external library, it has to be included in the build process in order for the application to successfully run. JavaFX has already been added as a Maven dependency to the POM file.
To run the application with Maven in IntelliJ, follow these steps:
1. Click the vertical "Maven" expansion tab which is on the right side of the IntelliJ window.
2. Expand the "Plugins" folder.
3. Expand the "javafx" folder.
4. Double-click "javafx:run" to run the project.
Important: This is exactly the same process that a learning assistant will use to launch your app when they manually grade it. Make sure your app can be launched using this process in your final submission to Gradescope.
Main class
The Main class represents the starting point of your application. When you use Maven to launch your app as described above, Maven will try to run the Main.main() method to launch your app. You are free to change the contents of the Main class as necessary, but keep in mind that the Main.main() method will always be the starting point of your application.
PuzzleLibrary
To make your life easier, the starter repository comes with a class called PuzzleLibrary. PuzzleLibrary exposes a class factory method, create(), which instantiates and returns a singleton List<Clues> list of Clues objects (see below for details). You can use this to set up a puzzle library in your model. If you want, you can add more puzzles to the puzzle library.
Model
All code related to the application's model should be placed in the model package.
Clues
Take a look at the Clues interface. Each Clues instance represents the clues for a single nonograms puzzle. In particular, a Clues instance should encapsulate the following concepts:
1. The height of the puzzle (i.e. the number of rows in the puzzle)
2. The width of the puzzle (i.e. the number of columns in the puzzle)
3. An array of integers for each row in the puzzle, representing the clues for that row
4. An array of integers for each column in the puzzle, representing the clues for that column
Clues instances are intended to be immutable---that is, the fields of a Clues instance should not change after instantiation.
CluesImpl
You must create a class named CluesImpl in the model package which implements the Clues interface. This is required for your submission to be autograded.
The CluesImpl class should expose a constructor with the following signature:
java public CluesImpl(int[][] rowClues, int[][] colClues) { // Constructor code here }
The rowClues parameter is a two-dimensional array containing the clues for each row of the puzzle (top-to-bottom), and the colClues parameter is a two-dimensional array containing the clues for the columns of the puzzle (left-to-right). Spaces can be represented by zeros in the clue arrays. For example, this puzzle is represented by the following instance:
```java int[][] rowClues = new int[][] { new int[] {0, 2}, new int[] {1, 2}, new int[] {0, 3}, new int[] {0, 3}, new int[] {1, 1}, }; int[][] colClues = new int[][] { new int[] {1, 1}, new int[] {0, 1}, new int[] {0, 3}, new int[] {0, 3}, new int[] {3, 1}, }; Clues example = new CluesImpl(rowClues, colClues); ```
Board
A Clues instance represents the clues for a puzzle, but doesn't handle the state to track whether individual cells are "shaded" or "eliminated". The Board interface is intended to track the array of states for each cell in a puzzle. In other words, a nonogram puzzle is a combination of a Clues object, representing the clues for the puzzle, and a Board object, representing the states of the puzzle's cells.
Instances of Board are not intended to be immutable. As the user clicks on different cells of the puzzle, the internal fields in the Board instance should change to reflect the new state. For example, if the user clicks on a cell to change it to be shaded, a field modification must take place inside th**e Board instance to reflect that the cell is now shaded. This can be accomplished via the toggleCellShaded() and toggleCellEliminated() methods.
Model
The Model interface represents the model of MVC, and therefore contains all data necessary to display the current state of the application. In particular, a Model instance should encapsulate the following concepts:
1. A library of available nonogram puzzles for the user to solve
2. A way to select which puzzle is currently active; for example, maybe an index into the puzzle library
3. The Clues and corresponding Board information for the currently active puzzle
4. A List<ModelObserver> of active model observers, represented by the ModelObserver interface (see below)
Notice that the Model interface extends both Board and Clues. That's because, per #3, a Model should represent the currently active Board and Clues for the currently active puzzle.
There are multiple ways to encapsulate a library of available puzzles. One way is to simply encapsulate a List<Clues> field to store the list of available puzzle Clues, and then re-initialize the encapsulated Board field(s) every time the user changes the active puzzle. This approach works but has a disadvantage: every time the user changes the active puzzle, their progress on the old puzzle will be forgotten. If you want your app to retain the user's progress when switching between puzzles, you'll have to encapuslate that information in your library somehow. One option is to create a new class, maybe called Puzzle, to encapsulate a Clues instance together with the corresponding Board fields for that puzzle. Then, you can store a List of Puzzle instances as your library, and keep track of the Clues for each puzzle along with the corresponding Board information.
ModelImpl
You must create a class named ModelImpl in the model package which implements the Model interface. This is required for your submission to be autograded.
The ModelImpl class should expose a constructor with the following signature:
java public ModelImpl(List<Clues> clues) { // Constructor code here }
The clues parameter is a list of Clues instances representing the library of available puzzles for the user to solve.
In order to implement the isSolved() method, you'll have to devise an algorithm to check whether the current state of the Board satisfies the corresponding Clues. This can be tricky, and will probably take some time to get right.
The ModelImpl class should be a "subject" with respect to the observer design pattern. This is because your view class will likely register itself as an active observer of the model, and will re-render itself in response to model changes. See the ModelObserver interface below for more information about how to implement this.
ModelObserver
the ModelObserver interface defines a single method, update(), and is used together with the ModelImpl class to implement the observer design pattern.
ModelImpl should therefore notify its active observers whenever any Model field value is changed. Hint: Below is a list of all Model interface methods. Exactly 4 of the 18 methods involve manipulating the state of the Model instance (not including addObserver() and removeObserver()); these 4 methods should therefore trigger the observer notify process.
``` Model methods: 1. int getPuzzleCount(); 2. int getPuzzleIndex(); 3. void setPuzzleIndex(int index); 4. void addObserver(ModelObserver observer); 5. void removeObserver(ModelObserver observer); 6. boolean isSolved();
Model methods inherited from Board: 7. boolean isShaded(int row, int col); 8. boolean isEliminated(int row, int col); 9. boolean isSpace(int row, int col); 10. void toggleCellShaded(int row, int col); 11. void toggleCellEliminated(int row, int col); 12. void clear();
Model methods inherited from Clues: 13. int getWidth(); 14. int getHeight(); 15. int[] getRowClues(int index); 16. int[] getColClues(int index); 17. int getRowCluesLength(); 18. int getColCluesLength(); ```
Controller
All code related to the application's controller should be placed in the controller package.
Remember, the controller package in MVC is intended to act as the "glue" between the model and the view. You are free to implement your controller as you see fit; the controller is not graded by the autograder. Instead, the controller will be manually graded when a learning assistant tries to play your game by running the Main.main() method!
However, to help you get started, an interface named Controller has been provided as a starting point. If you decide to use this interface, here's how you might use it.
First, add a new class called ControllerImpl to the controller package which implements the Controller interface. This class should encapsulate a single field: a Model instance.
Add a constructor to the ControllerImpl class that looks something like this:
java public Controller(Model model) { // Constructor code goes here }
The ControllerImpl object should "wrap" a Model instance. Most of the Controller methods will simply be delegated/forwarded directly to the encapsulated Model instance. However, a few methods, like prevPuzzle(), nextPuzzle, and randPuzzle(), will require a few lines of code to get right.
View
All code related to the application's view should be placed in the view package.
Remember, the view package in MVC is intended to hold all code related to the GUI. You are free to implement your view as you see fit; the view is not graded by the autograder. Instead, the view will be manually graded when a learning assistant tries to play your game by running the Main.main() method!
Regardless of the structure that you use for your view, you are required to use JavaFX as your GUI library. Therefore, the code in the view package will primarily create and manipulate JavaFX objects. To help you get started and figure out a good structure for your app, one class (AppLauncher) and one interface (FXComponent) are provided.
AppLauncher
By default, this class is the launching point of your application (although you can change that if you'd like). The Main class is set up to forward to AppLauncher, which extends Application and therefore launches the JavaFX GUI.
To write the view, you'll need to fill in the start() method in AppLauncher to actually set up and create your UI. Inside the start() method, you should create a Model object and a Controller object. You can use the provided PuzzleLibrary class to get a simple puzzle library for setting up the model.
FXComponent
Although you could put all your UI generation code directly in the start() method of AppLauncher, this might not be a good idea since it would clutter the method with a lot of JavaFX code. Instead, a better idea is to encapsulate each section of the UI in a separate class.
In GUI programming, it's common to call each "section" of the UI a "component". To this end, a (suggested) interface, named FXComponent, has been provided. You can use FXComponent to break up your interface into different smaller components. For instance, one FXComponent class, called PuzzleView, might display the clues and the game board inside a GridPanel. Another FXComponent class, called ControlView, might display the puzzle controls, including buttons, to move through the puzzle library. Finally, you might make a third FXComponent class, called MessageView, to show the "success" message when the user successfully finishes the puzzle.
Finally, the view must react to user actions, such as clicking on certain user interface elements. Do this by registering observers on the relevant
JavaFX UI component events. Sometimes, an application state change is necessary in response to a user action. For example, if the user clicks the
"next puzzle" button, the model must be instructed to go to the next puzzle. Make use of the controller's methods to do this. By utilizing the controller, you will enforce separation of concerns between your model and view, and enforce that the controller is an intermediary between the two.