| CPSC 329 | Software Development | Fall 2026 |
This lab provides practice with additional elements of JavaFX relevant for project P2: drag and drop, MVC, picking, multiple screens. All of these elements are covered in the slides from class and in the posted Klondike code; all but multiple screens were discussed in class.
Successful completion of this lab means that you:
can build a custom board component in layers: a background image with a grid of transparent, exactly-sized cells on top
can implement drag and drop in which the drop passes the user's intention to a controller rather than changing the game itself
can organize a JavaFX program with model-view-controller: a JavaFX-free model that the view listens to and a JavaFX-free controller that talks to the view only through an interface
can switch between screens in one window with scene.setRoot, through a navigation controller
can recognize and fix an overlay that keeps events from reaching the nodes under it
Labs are for learning and practice — you may help and be helped by others when it comes to figuring out what to do, but in the end you need to understand and be able to do the things for yourself.
This lab can be done individually or in pairs. If you work together, use pair programming rather than splitting things up and working in parallel.
You may not use AI to figure out what to do or to do the task for you.
due Fri 10/30 at the start of class
To hand in your work:
If you worked with someone else, add @author tags in the class comment for your main program naming both partners. Only one needs to hand in the lab.
Copy your entire ~/cs329/dev/lab8 directory into your handin directory. Make sure you end up with lab8 (directly) inside your handin directory and that it contains a src directory (with your code) — you shouldn't have a bunch of stuff loose in the top level of your handin directory or have folders nested deeper.
This lab handout itself and the slides and Klondike example from class (posted on the schedule page) should be your primary reference for these exercises.
For additional reference, consult the sources identified in the JavaFX reference provided in lab 7.
You have been provided with a peg solitaire game in which the model and most of the view are complete. You will fill in the parts that connect them, marked with TODO comments. The files:
PegBoard — the model: which holes exist, which have pegs, and the rules for jumping.
PegBoardListener — the model's Observer interface.
PegView — what the game controller may ask of a view (showMessage).
ScreenSwitcher — what the navigation controller may ask of the application.
NavigationController — moving between screens.
GameController — handles the moves the user asks for.
Main — the main program: creates and connects objects and switches screens.
StartView — the start screen.
GameView — the game screen: the board, the peg count, and a message line.
BoardView — the board component.
This program has the same structure as the Klondike example from class — match up these classes with those as you look at the slides.
In addition to the Java code, you have been provided with background images for two board layouts (board-english.png and board-european.png). Each image is 448×448 pixels, for a board of 7×7 squares of 64x64 pixels.
This lab requires JavaFX so you will need to have done the Eclipse setup from lab 6. If you haven't done that, or if you run into compilation and/or runtime errors working on this lab, go back to lab 6 for those instructions.
Create an Eclipse project and share it to a local Git repository:
Create a new Eclipse project named lab8. Remember that the project directory should end up under ~/cs329/dev, not in Eclipse's workspace.
Import all of the files from /classes/cs329/lab8 into your project. Make sure to preserve the directory structure with Java files in the src folder and images in images.
Confirm that the program runs correctly: run Main and you should see the start screen with an empty Board combo box. (Pressing Start doesn't do anything yet.) If there are JavaFX errors, check that you got things set up correctly in the Setup steps above.
Create a new (local) Git repository, create/set up .gitignore, and do the initial commit. Include a meaningful commit message.
Peg solitaire is played on a board of holes, all but one of them filled with pegs. A move jumps a peg over a neighboring peg (up, down, left, or right) into an empty hole, and the peg jumped over is removed. The game ends when no jump is possible; the goal is to leave as few pegs as you can. Two board layouts are provided: the English board (33 holes, a cross) and the European board (37 holes, the cross plus one more hole in each inside corner).
Work through the parts below in order. Places where you need to add code are marked with TODO comments in the provided code, labeled with the specific steps (e.g. 0a, 0b).
When the program starts, it shows a start screen where the user picks a board layout. Pressing Start should replace the start screen with the game screen, in the same window. In this part you'll fill in the start screen's controls and write the method that builds and connects a game's model, view, and controller.
Read about handling multiple screens: this wasn't covered in class yesterday (10/26) but it is included at the end of the posted slides. The provided code has the same structure.
Read Main.start. It creates the one NavigationController, passing this because Main is the ScreenSwitcher, creates the StartView and gives it the navigation controller, and shows it in a new Scene. showStart is a single line: scene_.setRoot(startView_).
Read NavigationController. It has no JavaFX in it, so it can't switch the scene itself — startGame(layout) just asks the ScreenSwitcher to showGame(layout).
Write code (0a): in the StartView constructor, fill the combo box with the layout names, select a default, and have the Start button pass the chosen layout to the navigation controller:
layout_.getItems().addAll(LAYOUT_NAMES);
layout_.setValue("English");
startButton.setOnAction(e -> navigation_.startGame(layout_.getValue()));
Setting a default means that the combo box always has a selection. Its value is read when Start is pressed, not before.
Write code (0b): in Main, write showGame. It has the same structure as KlondikeApp.showGame:
PegBoard board = new PegBoard(layout); GameView view = new GameView(board); GameController controller = new GameController(board,view); board.addListener(view); // model -> view view.setController(controller); // view -> controllers view.setNavigation(navigation_); view.refresh(board); // first drawing scene_.setRoot(view); // swap screens; the window stays
The first three lines create the model, the view, and the controller. The view takes the board so it can find out the layout and which holes exist; it reads the board but never changes it. The controller needs the view in its constructor, so the view gets the controller afterwards through setController.
board.addListener(view) means the board will tell the view whenever it changes.
view.refresh(board) draws the screen the first time. It's needed because the board only notifies its listeners when it changes.
scene_.setRoot(view) swaps the game screen in. The window and the scene stay the same; only the root node changes.
Run and check: choose a layout and press Start. You should see the background image of the chosen layout, the peg count (32 or 36), and an empty message line. There are no pegs yet, because there are no cells to draw them in until Part 1. Try both layouts. (If the window jumps to a different size, you created a new Scene instead of calling setRoot.)
Commit your work.
The board is designed as a StackPane with two layers: the background image at the bottom and a GridPane of cells on top. Each hole gets a cell: a StackPane exactly one square of the board in size. Each cell contains a slot (a Rectangle) which allows for decoration (e.g. highlighting a cell as a peg is dragged over it) and a peg if the hole has one. The cells let the image show through, line up exactly with the holes, and are where pegs are drawn and dropped. In this part you'll build the cells.
Read the BoardView constructor: it loads the background image for the layout, creates the GridPane, and adds both to the StackPane. A StackPane draws its children in order so the grid will be on top of the image. The board is fixed at exactly the size of the image so things will line up. Also read refresh: it draws a peg in each cell that has one and skips spots that have no cell. (Which is why there are no pegs yet.)
Write code (1a): in the BoardView constructor, make a cell for every hole on the board:
for ( int row = 0 ; row < BOARD_DIM ; row++ ) {
for ( int col = 0 ; col < BOARD_DIM ; col++ ) {
if ( board.isOnBoard(row,col) ) {
cells_[row][col] = makeCell(row,col);
grid_.add(cells_[row][col],col,row); // column first!
}
}
}
Note that GridPane.add takes the column first, then the row. The loop asks the model which spots are holes, so it builds 33 or 37 cells depending on the layout without any code that knows the shape of the board. (The specific configuration of holes for each layout is set up in PegBoard.)
Write code (1b): in makeCell, give the square a null fill so the image shows through (a Rectangle is black unless you say otherwise):
slot.setFill(null);
The slot is only decoration &mdash it doesn't need to receive events because the drop handlers are on the cell and a StackPane receives events anywhere inside its bounds. A null fill draws nothing and doesn't receive events, so drops go straight to the cell.
Write code (1c): also in makeCell, fix the cell's size at exactly one square:
cell.setMinSize(SQ_PIXSIZE,SQ_PIXSIZE); cell.setPrefSize(SQ_PIXSIZE,SQ_PIXSIZE); cell.setMaxSize(SQ_PIXSIZE,SQ_PIXSIZE);
Containers resize their children when they can. Setting the minimum, preferred, and maximum size to the same value means every cell is exactly 64×64, so the grid stays lined up with the holes in the image.
Run and check: press Start. Every hole except the center one should have a blue peg sitting in it. Then try the other layout: the European board has four more pegs but again should have only the center hole empty. If the pegs are in the wrong places, check the order of the arguments to grid_.add; if you see black squares, check 1b.
Commit your work.
When the user does something, the view passes the user's intention to the controller, the controller asks the model to make the move, and the model notifies its listener (the view), which redraws. If the move is refused (illegal move), nothing changes and the model doesn't notify anyone; the controller tells the view the reason instead. In this part you'll write the controller's method and the view's listener method. Since dragging pegs doesn't work yet, the game screen has a temporary Debug menu with a Make a Legal Move item for testing.
Read PegBoard.jump: if the move is legal, it changes the board and notifies the listeners; if not, it returns false and leaves the reason in lastError_ (accessed with getLastError()). Then read GameController (look at the instance variables) and PegView: the controller sees the view only as a PegView, so all it can do is show a message.
Write code (2a): in GameController, write moveRequested, replacing the placeholder return false;:
boolean moved = board_.jump(fromRow,fromCol,toRow,toCol);
if ( moved ) {
view_.showMessage("");
} else {
view_.showMessage(board_.getLastError());
}
return moved;
After a legal move the controller only clears the message. It doesn't redraw anything (the board's notification takes care of that).
Write code (2b): in GameView, write the listener method boardChanged so the view redraws when the board changes:
refresh(board);
Run and check: start a game (English) and choose Debug → Make a Legal Move. The peg in row 3, column 1 jumps to the center (rows and columns are numbered from 0, starting at the top left) and the count drops to 31. Choose it again: nothing moves, and the message line says "There's no peg there to move." If the message changes but pegs never move, check that showGame calls board.addListener(view).
Trace: set a breakpoint in GameView.refresh and run the program using the debugger. Continue after the first time the breakpoint is reached, then choose Debug → Make a Legal Move in the game and observe the call stack in the debugger when the breakpoint is hit again — this shows you most of the sequence of calls that happen when an event occurs.
Commit your work.
Now the user will be able to move pegs by dragging them. A drag and drop takes handlers at both ends. On the peg, one handler starts the drag when the peg is dragged, carrying where the peg came from as a string (e.g. "3,1"), and another cleans up when the drag ends. On the cells, one handler says the cell accepts the drop, and another handles the drop. You'll write all four. The drop handler doesn't decide whether the move is legal; instead, it passes the user's intention to the controller, just like Klondike's drop handler calls playWasteTo.
Write code (3a): in makePeg (BoardView), start a drag and drop when the peg is dragged:
peg.setOnDragDetected(e -> {
Dragboard db = peg.startDragAndDrop(TransferMode.MOVE);
ClipboardContent content = new ClipboardContent();
content.putString(row + "," + col); // where the peg comes from
db.setContent(content);
peg.setOpacity(0.4); // dim the peg being moved
e.consume();
});
startDragAndDrop starts the gesture with the peg as its source. Whatever is being moved goes on the Dragboard, like copying to the clipboard; here, that's the peg's position as a string. Dimming the peg shows which one is being moved.
Write code (3b): also in makePeg, clean up when the drag ends:
peg.setOnDragDone(e -> {
peg.setOpacity(1.0);
e.consume();
});
DRAG_DONE goes to the peg the drag started from, whether or not the drop worked, so this undoes the dimming after a refused move. (After a legal move, the board is redrawn with new pegs anyway.)
Run: try dragging a peg. The peg dims, but the cursor shows "not allowed" everywhere (if your platform supports that) and nothing can be dropped, because no node has said it accepts a drop. When you let go, the peg goes back to normal. (Note that the drag looks like you are dragging a piece of paper, not a peg — fixing that is an optional "if you have time" step at the end of the lab.)
Write code (3c): in makeCell, accept drags that carry a string:
cell.setOnDragOver(e -> {
if ( e.getDragboard().hasString() ) {
e.acceptTransferModes(TransferMode.MOVE);
}
e.consume();
});
DRAG_OVER happens over and over while a peg is dragged across the cell. Accepting the transfer mode says a drop would be welcome here, which changes the cursor.
Write code (3d): also in makeCell, handle the drop by passing the move to the controller:
cell.setOnDragDropped(e -> {
String[] from = e.getDragboard().getString().split(",");
int fromRow = Integer.parseInt(from[0]);
int fromCol = Integer.parseInt(from[1]);
controller_.moveRequested(fromRow,fromCol,row,col);
e.setDropCompleted(true);
e.consume();
});
Because the handlers are written inside makeCell(row, col) and makePeg(row, col), row and col are parameters, so each handler knows which cell or peg it belongs to. (Writing them directly in the loop in the constructor wouldn't compile, because lambdas can't use loop variables that change; that's why the Klondike code copies the loop variable into final int col first.) setDropCompleted(true) only says that the gesture finished; whether the move was legal is the controller's business.
Run and check: legal jumps work by dragging. An illegal drop, such as dropping a peg on the hole next to it, shows the model's reason in the message line, and the peg goes back to normal. If the cursor still shows "not allowed" (again, if your platform supports that), check that 3c calls acceptTransferModes.
Now that dragging works, remove the Debug menu from GameView: delete the lines that build it, and remove debugMenu from the MenuBar.
Commit your work.
JavaFX gives a mouse or drag event to the topmost node under the pointer. That's usually what you want, but a decoration drawn on top of the board, even a translucent one, gets the events instead of the cells underneath it. In this part you'll add such an overlay, see what goes wrong, and fix it. The drop zone hint in the Klondike example is this kind of overlay.
Read showRowHint and hideRowHint in BoardView: they add and remove a translucent white band across one row of the grid, on top of the cells.
In makePeg, add the line
showRowHint(row);right before e.consume() in the DRAG_DETECTED handler so that dragging a peg highlights its row. Also add the line
hideRowHint();right before e.consume() in the DRAG_DONE handler to remove the highlight at the end of the drag.
Run and observe: drag a peg sideways, within its own row. The drop fails silently: the cursor shows "not allowed" (if your platform supports that) and nothing happens. Dragging a peg up or down still works. Why?
Write code (4): in showRowHint, make the band ignore the mouse so events go to whatever is underneath it:
hint_.setMouseTransparent(true);
Run and check: sideways jumps work again, with the band showing.
Commit your work.
Finally, the user needs a way back to the start screen: the Game → New Game menu item at any time and a Play Again button when the game is over. Both go through the same NavigationController as Start, this time calling newGame(), which asks Main to showStart(). Pressing Start again then builds a fresh game, view, and controller.
Write code (5a): in GameView, write confirmNewGame, which the New Game menu item calls. Abandoning a game in progress deserves a confirmation, as in lab 7:
Alert alert = new Alert(AlertType.CONFIRMATION,"Abandon this game?",
ButtonType.YES,ButtonType.NO);
Optional<ButtonType> result = alert.showAndWait();
if ( result.isPresent() && result.get() == ButtonType.YES ) {
navigation_.newGame();
}
Write code (5b): in the GameView constructor, have the Play Again button go back to the start screen:
playAgain_.setOnAction(e -> navigation_.newGame());
Write code (5c): in GameView.refresh, show the Play Again button only when the game is over:
playAgain_.setVisible(board.isGameOver());
Why a button and not an Alert at the end of the game? refresh runs inside the drop handler (by way of jump and boardChanged) while the drag and drop is still finishing — opening a dialog there causes trouble.
Run and check: start a game and choose New Game — No keeps the game and Yes goes back to the start screen. Then play a game to the end: when no jump is left, "Game over" and the Play Again button appear beside the peg count and Play Again goes back to the start screen. Either way, Start builds a fresh board.
Commit your work.
This part is optional. If you have time, add one or more of the following: (see the slides from class for hints/examples of these for Klondike)
Drop-target ring: in makeCell, add DRAG_ENTERED and DRAG_EXITED handlers that give the cell's slot a thick dark stroke while a peg is over it and remove it when the peg leaves.
Drag image: in the DRAG_DETECTED handler, call db.setDragView(...) with a snapshot of the peg so a copy of the peg follows the pointer. Take the snapshot with SnapshotParameters whose fill is Color.TRANSPARENT, otherwise the image has a white box around it.
Controller tests: write JUnit tests for GameController using a fake view as in the Klondike example: a FakeView class that implements PegView and records the last message. Test that a refused move shows the board's reason and leaves the board unchanged and that a legal move clears the message. Then test NavigationController with a fake ScreenSwitcher.
A binding: make the hidden Play Again button take up no space with playAgain_.managedProperty().bind(playAgain_.visibleProperty()).
Keyboard alternative: let the user play without a mouse. Make the board focusable, show a cursor ring that the arrow keys move, pick a peg up with Space, and drop it with Enter, calling the same moveRequested.