Session 7 · Programming Technology
Board games: update only what changed
A Color clicker that redraws every button after each click becomes an MV program whose model returns the changed positions, so the view refreshes five buttons instead of n². Then a walk through the sample solution of the second assignment, and the shared requirements and tips for every task.
Last session the view redrew the whole window after every event, and for a flashcard trainer that was the right call. A board game has a grid of buttons, though, and one move usually changes only a few of them. This session starts from a board game that works but redraws every button after every click, and turns it into an MV program where the model reports what changed and the view updates only that.
The second half is a complete sample solution of the second assignment's first task – the drop-in tic-tac-toe – with its model, view, tests and documentation. Its structure is the one to copy.
Optimising is the model's job, not the view's. The model knows exactly which fields it just changed – so it should say so. A view that has to guess what changed (by comparing old and new state) is a sign that something is in the wrong place.
The game: Color clicker
The board is an n × n grid of buttons, all empty at the start. Clicking an empty button colours it and four more random empty buttons with the same random colour, and writes the number of the click on them. Clicking a coloured button does nothing. The game ends when every button is coloured, and shows how long it took. Game ▸ New offers 5×5, 10×10, 15×15 and 20×20 boards; a Timer updates the elapsed time every 10 ms.
The two versions, Colorclicker and ColorclickerOptimized, look and play the same. The difference is how the code is organised – and whether a click redraws 5 buttons or 400.
The starting version: what happens on a click
| Class | Job |
|---|---|
Field | one field: a colour (java.awt.Color, null if empty) and a number |
Board | the n × n array of fields; isOver() checks whether every field is coloured |
BoardGUI | the button grid, the clock – and the rules: the listener decides what gets coloured; refresh() redraws every button |
ColorClickerGUI | the window and the menu; a new game puts a new BoardGUI into the window |
The click handler checks whether the field is empty, picks a colour, increments the click counter and sets the field. Then it takes positions from the end of a pre-shuffled list until it finds four empty ones and sets those too. Finally it calls refresh():
Colorclicker – the listener, shortened
// Colorclicker (before): the rule lives in the button's listener
public void actionPerformed(ActionEvent e) {
if (board.get(x, y).getColor() == null) {
Color color = new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256));
board.get(x, y).setColor(color);
board.get(x, y).setNumber(++clickNum);
for (int i = 0; i < NUM_COLORED_FIELDS;) { // 4 more empty fields
Point point = points.remove(points.size() - 1);
if (board.get(point).getColor() == null) { /* color it, i++ */ }
}
refresh(); // ...and redraw everything
}
}Colorclicker – refresh()
public void refresh() {
for (int i = 0; i < board.getBoardSize(); ++i) {
for (int j = 0; j < board.getBoardSize(); ++j) { // n × n buttons
Field field = board.get(i, j);
buttons[i][j].setBackground(field.getColor());
// ... setText(number)
}
}
if (board.isOver()) { /* scans the n × n fields again */ }
}What's wrong with it?
- Wasted work. A click changes 5 fields, but
refresh()sets n² buttons. EachsetBackgroundandsetTextasks for a repaint – on 20 × 20 that is 800 calls per click instead of 10. AndisOver()scans all n² fields on every click, when counting the empty ones would do. - The rules are in the view. That 4 extra fields get coloured, that the colour is random, that the number grows – all of it is in the listener, so none of it can be tested with JUnit.
- The model depends on AWT.
Fieldstores ajava.awt.Color,Boardusesjava.awt.Point. - Small bugs. On a new game the old
BoardGUI'sTimerkeeps running and keeps the old board in memory. And if the number of fields were not divisible by 5 (say 7 × 7), the list of positions would run out on the last click and the program would crash. The menu only offers sizes divisible by 5, so it never shows – but the bug is there.
MV, and only the changed buttons
ColorclickerOptimized moves the program into the model–view structure from last session, because the optimisation needs it: only a model that owns the rules can say what a click changed.
| Class | Layer | Job |
|---|---|---|
Position | model | a field's place: row, column (a record) – instead of java.awt.Point |
Field | model | number (0 = empty) and colour as an int (0xRRGGBB); only the Board may colour it |
Board | model | the board, click count, empty count, the time – and the rules: click returns which fields changed |
BoardGUI | view | button grid and clock; the listener calls the model and refreshes only the returned fields |
ColorClickerGUI | view | window and menu; stops the old clock on a new game |
BoardTest | test | 19 JUnit tests for the model |
The model says what changed
The key is the return value of click. The model knows exactly which fields it modified, so it returns them in a list:
Board.click
public List<Position> click(int row, int col) {
if (!canClick(row, col)) {
throw new IllegalStateException("Cannot click (" + row + ", " + col + ")");
}
clickCount++;
int rgb = random.nextInt(COLOR_COUNT); // 0x000000 .. 0xFFFFFF
List<Position> changed = new ArrayList<>();
changed.add(new Position(row, col)); // the clicked field
List<Position> empty = emptyPositions();
empty.remove(new Position(row, col));
Collections.shuffle(empty, random);
changed.addAll(empty.subList(0, Math.min(EXTRA_FIELDS, empty.size()))); // + at most 4
for (Position position : changed) {
get(position).color(clickCount, rgb);
emptyCount--;
}
return changed; // the model says what changed
}- The colour is an int (
0xRRGGBB, like#RRGGBBin CSS), not ajava.awt.Color. The model has no AWT import at all; the view makes anew Color(rgb). (Coloris only a value class, not a component – but "nojava.awtin the model" is an easier rule to keep than a rule with exceptions.) Math.min(EXTRA_FIELDS, empty.size()): if fewer than 4 empty fields are left at the end, those get coloured – a 7 × 7 board would work.- A field that can't be clicked (coloured, or game over) is reported by
canClick, andclickthrows on it. The view asks first and only then calls – the same guard pattern asrequireCurrentCardlast session.
The view: the path of a click
BoardGUI.click
private void click(int row, int col) {
if (!board.canClick(row, col)) {
return; // colored field or game over: nothing happens
}
List<Position> changed = board.click(row, col);
for (Position position : changed) { // the optimisation: only these
refreshField(position);
}
refreshTime();
if (board.isOver()) {
timer.stop();
JOptionPane.showMessageDialog(boardPanel, "You have won in " + board.getElapsedMillis() + " ms.");
}
}refreshField(position) is the only place where the view reads one field of the model. refreshAll() runs only at the start, and calls the same method for every field. So the "optimised" and the "full" refresh share their code and can never disagree.
private void refreshField(Position position) {
Field field = board.get(position);
JButton button = buttons[position.row()][position.col()];
if (field.isEmpty()) {
button.setBackground(null);
button.setText("");
} else {
button.setBackground(new Color(field.getRgb())); // int in the model, Color here
button.setText(String.valueOf(field.getNumber()));
}
}
private void refreshAll() { // only at the start – same code, every field
for (int row = 0; row < board.getSize(); row++) {
for (int col = 0; col < board.getSize(); col++) {
refreshField(new Position(row, col));
}
}
}Swing is fast, and on these boards you won't see the difference. The principle is general, though: the view refreshes what changed, and the model tells it what that was. In the board games of the assignment, a move usually affects a few fields (Amoba's drop returns where the piece landed). If the whole board can change – a row of the Rubik board, say – then a full refresh is the right answer.
Counters instead of scans, Random from outside
public boolean canClick(int row, int col) {
return !isOver() && get(row, col).isEmpty();
}
public boolean isOver() {
return emptyCount == 0; // a counter, not a scan of n × n fields
}The game is over when the empty-field counter reaches zero – no need to look at n² fields. The time is measured by the model too, and stops when the game ends.
Testable randomness
public Board(int size) {
this(size, new Random()); // the program: really random
}
public Board(int size, Random random) {
this.random = random; // a test: new Random(42) – same "random" every run
// ...
}The tests create the board with new Random(42). With a fixed seed, the "random" colours and fields are the same on every run, so the test is predictable: one click colours the clicked field and 4 more, all with the same colour and number; exactly the returned fields change and the rest of the board stays untouched; an illegal click throws and changes nothing; a 2 × 2 board is finished by one click.
| Colorclicker | ColorclickerOptimized | |
|---|---|---|
| Where the rules are | BoardGUI.ButtonListener | Board.click |
| What changed? | the view doesn't know, refreshes all | click returns the list of positions |
| Position of a field | java.awt.Point | a Position record |
| Colour | java.awt.Color in the model | int in the model, Color in the view |
| Game over? | scan n² fields | empty-field counter |
| Time | measured by the view | measured by the model, stops at the end |
| New game | old Timer keeps running | boardGUI.stop() |
| Fewer than 4 empty fields | exception (if size not divisible by 5) | the rest gets coloured |
| Testing | only by clicking | 19 JUnit tests |
The sample solution: drop-in tic-tac-toe
Amoba is a complete solution of task 1 of the second assignment, drop-in tic-tac-toe (potyogós amőba). Two players, X and O, take turns dropping their mark into a column of an n × m board; the mark falls to the lowest empty field of that column. Whoever first has four in a row wins; a full board is a draw. At the end the program shows the result and starts a new game automatically. Boards: 8 × 5, 10 × 6, 12 × 7.
It shows what a submission-ready game looks like: a Swing-free model, a view that only displays and forwards, 43 JUnit tests (at least one per rule of the task), and documentation with the sections the assignment asks for.
Structure
| Class | Job | Doesn't |
|---|---|---|
Board | the board's own rules: dropping (drop(col, player)), full column, full board, the four-in-a-row check | know whose turn it is or whether the game is over |
Game | the course of the game: whose turn, canDrop, drop, over, winner, newGame | draw anything |
Player, BoardSize, Position | enum X/O with other(); enum of the three sizes; record of row and column | – |
AmobaGUI | window, menu, button grid, status bar, result dialog | store game state or decide where a mark lands |
Why two model classes? Board knows the rules of the board, Game the rules of the game. Both stay short, and both can be tested separately – a BoardTest can put four X marks next to each other without O ever moving.
One move, one button
Every button of a column does the same thing, so its listener (ColumnListener) only needs the column. Game.drop returns where the mark landed – the Color clicker lesson applied: the view refreshes exactly one button.
Game.drop – three outcomes, exactly the three cases of the task
public Position drop(int col) {
if (over) {
throw new IllegalStateException("The game is over");
}
Position landed = board.drop(col, currentPlayer);
if (board.hasLineThrough(landed)) {
winner = currentPlayer; // a win
over = true;
} else if (board.isFull()) {
over = true; // a draw
} else {
currentPlayer = currentPlayer.other(); // the other player's turn
}
return landed;
}The win is checked before the draw: if the last mark fills the board and makes four, that is a win. The view follows the same ask-then-act pattern as the Color clicker:
AmobaGUI.drop
private void drop(int col) {
if (!game.canDrop(col)) {
return; // full column or game over: no message, nothing
}
Position landed = game.drop(col);
refreshField(landed); // exactly one button changed
refreshStatus();
if (game.isOver()) {
showResult(); // modal dialog: the final board stays visible
game.newGame(); // automatic new game, same size
refreshAll();
}
}Four in a row: look only through the last piece
If a move wins, the new mark is part of the four – so it's enough to search through that one field. For each direction, count equal marks forward and backward, and add the field itself:
Board.hasLineThrough
private static final int[][] DIRECTIONS = {{0, 1}, {1, 0}, {1, 1}, {1, -1}};
public boolean hasLineThrough(Position position) {
for (int[] direction : DIRECTIONS) {
int dRow = direction[0];
int dCol = direction[1];
int length = 1 + count(position, dRow, dCol) + count(position, -dRow, -dCol);
if (length >= WINNING_LENGTH) {
return true;
}
}
return false;
}The directions live in one array, named. Leaving out or adding a direction is one line – and one test documents it.
The states of a game
The result dialog is modal: while it is open the buttons can't be clicked and the final position stays visible. Calling JOptionPane from the listener is fine here, because game.drop has already returned by then.
Read the task, write down your interpretation
The task sheet mentions horizontal or diagonal fours. The sample follows the well-known game (Connect Four) and counts a vertical four as a win too – a test (fourInAColumnWins) documents that choice, and removing {1, 0} from DIRECTIONS would undo it. Things the task leaves open, decided and written down in the documentation:
- X always starts, in every new game.
- Clicking any button of a column drops into that column.
- Clicking a full column, or after the game is over, does nothing – no error message.
- A last move that fills the board and makes four is a win, not a draw.
- The new game starts after the result dialog is closed, on the same board size.
- The buttons are not disabled at the end, because a new game follows immediately.
Sizes as an enum, the menu built from it
public enum BoardSize {
SMALL(8, 5), MEDIUM(10, 6), LARGE(12, 7);
// ... columns, rows, toString() -> "8 × 5"
}
for (BoardSize size : BoardSize.values()) { // the menu is built from the enum
JMenuItem item = new JMenuItem(size.toString());
item.addActionListener(e -> newGame(size));
newGameMenu.add(item);
}The documentation
The sample's description is a Markdown README only because that reads well on GitHub. Your documentation has to be a DOCX or PDF file – with the same sections:
- The task and its analysis: the task text, the rules as a numbered table (rule → where in the code), and the decisions the task left open.
- The user interface: what the window looks like and how it is used.
- The structure: the layers, the files, a UML class diagram, each class and its methods, design decisions.
- Event–handler pairs: event, source, handler, what it does.
- Sequence and state diagrams for a move and for the end of the game.
- Tests: which test class checks what.
Draw the diagrams with any tool – draw.io, StarUML, PlantUML, Mermaid, Visual Paradigm, or by hand and scanned. What matters is that they are in the document as images and match the code.
The second assignment: requirements and tips
The shared requirements of the task sheet, and where the sample solves them:
| Requirement | How |
|---|---|
| User-friendly, easy to use | button grid, menu, status bar (whose turn, move count); an illegal click is ignored, not an error message |
| Object-oriented; layers are not mandatory but strongly recommended | model–view: the rules are testable, the view is simple |
| The window size follows the board | frame.pack() after rebuilding the button grid |
| New game with a chosen size | an enum of sizes, the menu built from values(); newGame(size) |
| Detect the end, show the winner, start a new game automatically | model: isOver() / getWinner(); view: JOptionPane, then newGame() and a full refresh |
| Documentation with analysis, structure, UML class diagram, event–handler pairs; DOCX or PDF | the sections above |
| Tests (asked at the presentation) | JUnit for the model, one test per rule |
For each task: what is the model state, what is a move, when is it over, and what to watch out for.
| # | Task | Model state | A move | The end | Watch out for |
|---|---|---|---|---|---|
| 1 | Drop-in tic-tac-toe (Potyogós amőba) | Player[][], whose turn | drop(col): to the lowest empty field | 4 in a line; full board | see the sample |
| 2 | Push-out (Kitolás) | Player[][] stones, the selected stone, moves left | select, then a direction: the stones in the row/column shift, the outermost falls off | 5n moves used up; more stones wins | a move is two clicks: the model stores whether a stone is selected; the random start comes from a Random, with a fixed seed in tests |
| 3 | Hunt (Vadászat) | positions of the fugitive and the 4 hunters, whose turn, move count | a selected piece + an adjacent empty field | the fugitive can't move (within 4n moves) | "can't move" = its 4 neighbours are taken or off the board; show the move count all the time |
| 4 | Tic-tac-toe with removal (Kiszúrós amőba) | Player[][], whose turn | a mark on an empty field, then removal: a line of 3 removes 1, a line of 4 removes 2 own marks at random | 5 in a line; full board | you need the length of the longest line, not just "is there a 4"; the removed fields must be refreshed too – the move returns a list of changed positions |
| 5 | Black hole (Fekete lyuk) | ships per colour, the hole, whose turn, ships in the hole | a selected ship + a direction: it slides until an edge, the hole or another ship stops it | half of a player's ships are in the hole | the slide is a loop in the model; a ship reaching the hole leaves the board |
| 6 | Game of fours (4-es játék) | int[][] values 0–4, who brought each to 4, points | a field and its 4 neighbours +1 (if below 4); whoever brings one to 4 scores | every field is 4 | a move can change 5 fields; show the points all the time |
| 7 | Breakthrough (Áttörés) | Player[][], whose turn, selected piece | select, then target: forward straight or diagonally to an empty field, or capture diagonally | a piece reaches the far row | "forward" depends on the player |
| 8 | Knights' tournament (Lovagi torna) | knight positions, field colours, whose turn | a selected knight + an L-shaped target | all fields coloured; 4 of a colour in a line | the 8 possible targets of an L-move; the four-check as in Amoba, on colours |
| 9 | Rubik board (Rubik tábla) | int[][] colours, move count | cyclic shift of a row or column | every row or every column is one colour | shuffle with valid moves from the solved state so it's always solvable; a whole row/column changes – refresh that |
| 10 | Rubik clock (Rubik óra) | 9 clock values (1–12), move count | a button: the 4 neighbouring clocks +1 | all 9 show 12 | 3 × 3 clocks with 4 buttons between them – GridLayout alone won't do (two grids, or a 5 × 5 grid); after 12 comes 1 (% 12) |
Practice
Task 1
Do the optimisation yourself
Start from the original Colorclicker. Move the click rule into Board, make it return the changed fields, and make the view refresh only those. Then fix the two small bugs: the running old Timer and the crash when fewer than four empty fields are left.
Approach
First the method signature: click belongs in Board and returns a list. Then split refresh() into a per-field method and a loop that calls it. Only then change the colour type to int and Point to a record.
// in Board – the rule moves here and reports what it changed
public List<Point> click(int x, int y) { ... return changed; }
// in the listener – only the returned fields
for (Point p : board.click(x, y)) {
refreshField(p);
}Task 2
Test that exactly the returned fields change
Write a JUnit test for ColorclickerOptimized: after a click, every returned position was empty before and is coloured now, and every other position is unchanged.
Approach
Use a fixed seed so the test is repeatable. Take a snapshot of the empty positions before the click and compare it to the one after.
Board board = new Board(10, new Random(42));
board.click(5, 5);
List<Position> emptyBefore = board.emptyPositions();
List<Position> changed = board.click(1, 1);
// every changed position was empty before and isn't now;
// every other position that was empty is still emptyTask 3
Undo in Amoba
Add Game ▸ Undo: it takes back the last mark (and can be repeated). Which class remembers the moves? What does undo return to the view? What happens after the game is over?
Approach
The model keeps a stack of landing positions. Undo pops one, clears that field on the board, gives the turn back and returns the position – so the view refreshes one button, as after a drop. With an empty stack, throw IllegalStateException and add a canUndo() query for the menu item.
private final Deque<Position> history = new ArrayDeque<>();
// in drop(col), after a successful board.drop:
history.push(landed);
// undo(): the last piece leaves the board, the turn goes back
Position last = history.pop();
// board.clear(last); currentPlayer = currentPlayer.other(); return last;Task 4
Plan your own task
For your second-assignment task, write down: the model's fields; the move operation(s) and their return value; the "can I?" query for each operation; the condition of the end; and which fields a move can change. Then list one test per rule.
Check your plan
Every field of your model should be readable through a query, and every operation should have a matching can… query. If a move needs two clicks, the first click's result is a field in the model. If you can't name what a move changes, the view will end up refreshing everything – fine for a Rubik board, a smell for most others.
Summary
- Changed positions
- what a model operation returns so the view can refresh only those fields
- refreshField / refreshAll
- one method for one field; the full refresh is a loop over it, so the two can't differ
- canX / x
- a query ("may I?") and the operation that throws if called illegally
- Fixed seed
new Random(42): the same "random" sequence on every run, so tests are predictable- Position record
- row and column as a value – the model's replacement for java.awt.Point
- The model knows what it changed – it returns it; the view refreshes exactly that.
- Keep the model free of AWT: colours as int, places as a record; Random through the constructor.
- Counters beat scans: an empty-field counter answers "is it over?" in constant time.
- Stop the old Timer when a new game starts.
- Amoba: Board for the board's rules, Game for the game's; drop returns where the mark landed; the four-check looks only through the last mark.
- Your documentation: analysis, UI, structure with a UML class diagram, event–handler pairs, diagrams, tests – as DOCX or PDF.