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.

17 min readThe model returns what changedrefreshField vs refreshAllNo AWT in the modelSeeded Random in testsFour-in-a-row through the last pieceSecond assignment requirements

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

ClassJob
Fieldone field: a colour (java.awt.Color, null if empty) and a number
Boardthe n × n array of fields; isOver() checks whether every field is coloured
BoardGUIthe button grid, the clock – and the rules: the listener decides what gets coloured; refresh() redraws every button
ColorClickerGUIthe 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. Each setBackground and setText asks for a repaint – on 20 × 20 that is 800 calls per click instead of 10. And isOver() 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. Field stores a java.awt.Color, Board uses java.awt.Point.
  • Small bugs. On a new game the old BoardGUI's Timer keeps 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.

ClassLayerJob
Positionmodela field's place: row, column (a record) – instead of java.awt.Point
Fieldmodelnumber (0 = empty) and colour as an int (0xRRGGBB); only the Board may colour it
Boardmodelthe board, click count, empty count, the time – and the rules: click returns which fields changed
BoardGUIviewbutton grid and clock; the listener calls the model and refreshes only the returned fields
ColorClickerGUIviewwindow and menu; stops the old clock on a new game
BoardTesttest19 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 #RRGGBB in CSS), not a java.awt.Color. The model has no AWT import at all; the view makes a new Color(rgb). (Color is only a value class, not a component – but "no java.awt in 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, and click throws on it. The view asks first and only then calls – the same guard pattern as requireCurrentCard last session.
before – refresh(): every buttonn × n = 100 updatesafter – refreshField × changed5 updatesbuttons set per clickboardbeforeafter5 × 525510 × 10100515 × 15225520 × 204005Blue = a button whose setBackground and setText run after the click.
Fig. 1 · Per click the starting version touches every button; the optimised one touches the five the model returned. On a board this small you won't see the difference – the principle is what matters.

The view: the path of a click

PlayerButtonListener(row, col)BoardGUIthe viewBoardthe modelclickclick(row, col)canClick(row, col)trueclick(row, col)colour 1 + 4 fieldschanged: 5 positionsrefreshField(p) × 5refreshTime()isOver()false – until the last click
Fig. 2 · The rule runs in the model; the view asks, calls, and refreshes exactly what came back. If canClick is false, the view simply returns – no message, no exception.

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.

ColorclickerColorclickerOptimized
Where the rules areBoardGUI.ButtonListenerBoard.click
What changed?the view doesn't know, refreshes allclick returns the list of positions
Position of a fieldjava.awt.Pointa Position record
Colourjava.awt.Color in the modelint in the model, Color in the view
Game over?scan n² fieldsempty-field counter
Timemeasured by the viewmeasured by the model, stops at the end
New gameold Timer keeps runningboardGUI.stop()
Fewer than 4 empty fieldsexception (if size not divisible by 5)the rest gets coloured
Testingonly by clicking19 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

view – SwingAmobaGUIwindow, menu, button grid, status barColumnListenerone per button, holds its columndrop(col)model – no javax.swing, no java.awtGamewhose turn, drop, over, winnerBoardfields, drop, hasLineThroughPlayerenum: X, OBoardSizeenum: 3 sizesPositionrecord: row, colcanDrop, drop, newGameget(row, col)tests – JUnitBoardTestGameTest
Fig. 3 · Two layers and a test layer. The dependency points one way: the view knows the model, the model knows nothing about the view.
ClassJobDoesn't
Boardthe board's own rules: dropping (drop(col, player)), full column, full board, the four-in-a-row checkknow whose turn it is or whether the game is over
Gamethe course of the game: whose turn, canDrop, drop, over, winner, newGamedraw anything
Player, BoardSize, Positionenum X/O with other(); enum of the three sizes; record of row and column–
AmobaGUIwindow, menu, button grid, status bar, result dialogstore 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.

8 × 5 board, column 3XOOXX●● = the button clicked (row 0); X lands in row 2.ColumnListener(col = 3)game.drop(3)returns Position(2, 3)refreshField(Position(2, 3))
Fig. 4 · The row you click doesn't matter, the task says so. The model computes the landing row and returns the position; the view never computes it.

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:

OOXOXXXXOnewcount(0, −1) = 2count(0, 1) = 1DIRECTIONS – and each one backwards{0, 1}{1, 0}{1, 1}{1, -1}length = 1 + count(+d) + count(−d)1 + 2 + 1 = 4 → win
Fig. 5 · The new mark can be at either end or in the middle of the line, and five in a row also counts. No scan of the whole board, and no counter that has to be reset at the end of each row.

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

new GameRunningX or O to moveWinwinner = who moved lastDrawboard full, no fourdrop: the other player's turndrop: four in a linedrop: board fullnewGame() – automatic, after the dialogcanDrop() is false,drop() throws
Fig. 6 · A state diagram like this belongs in your documentation. In the end states the model refuses moves; the view starts the next game after the modal dialog.

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:

  1. 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.
  2. The user interface: what the window looks like and how it is used.
  3. The structure: the layers, the files, a UML class diagram, each class and its methods, design decisions.
  4. Event–handler pairs: event, source, handler, what it does.
  5. Sequence and state diagrams for a move and for the end of the game.
  6. 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:

RequirementHow
User-friendly, easy to usebutton 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 recommendedmodel–view: the rules are testable, the view is simple
The window size follows the boardframe.pack() after rebuilding the button grid
New game with a chosen sizean enum of sizes, the menu built from values(); newGame(size)
Detect the end, show the winner, start a new game automaticallymodel: isOver() / getWinner(); view: JOptionPane, then newGame() and a full refresh
Documentation with analysis, structure, UML class diagram, event–handler pairs; DOCX or PDFthe 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.

#TaskModel stateA moveThe endWatch out for
1Drop-in tic-tac-toe (Potyogós amőba)Player[][], whose turndrop(col): to the lowest empty field4 in a line; full boardsee the sample
2Push-out (Kitolás)Player[][] stones, the selected stone, moves leftselect, then a direction: the stones in the row/column shift, the outermost falls off5n moves used up; more stones winsa 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
3Hunt (Vadászat)positions of the fugitive and the 4 hunters, whose turn, move counta selected piece + an adjacent empty fieldthe 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
4Tic-tac-toe with removal (Kiszúrós amőba)Player[][], whose turna mark on an empty field, then removal: a line of 3 removes 1, a line of 4 removes 2 own marks at random5 in a line; full boardyou 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
5Black hole (Fekete lyuk)ships per colour, the hole, whose turn, ships in the holea selected ship + a direction: it slides until an edge, the hole or another ship stops ithalf of a player's ships are in the holethe slide is a loop in the model; a ship reaching the hole leaves the board
6Game of fours (4-es játék)int[][] values 0–4, who brought each to 4, pointsa field and its 4 neighbours +1 (if below 4); whoever brings one to 4 scoresevery field is 4a move can change 5 fields; show the points all the time
7Breakthrough (Áttörés)Player[][], whose turn, selected pieceselect, then target: forward straight or diagonally to an empty field, or capture diagonallya piece reaches the far row"forward" depends on the player
8Knights' tournament (Lovagi torna)knight positions, field colours, whose turna selected knight + an L-shaped targetall fields coloured; 4 of a colour in a linethe 8 possible targets of an L-move; the four-check as in Amoba, on colours
9Rubik board (Rubik tábla)int[][] colours, move countcyclic shift of a row or columnevery row or every column is one colourshuffle with valid moves from the solved state so it's always solvable; a whole row/column changes – refresh that
10Rubik clock (Rubik óra)9 clock values (1–12), move counta button: the 4 neighbouring clocks +1all 9 show 123 × 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 empty

Task 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.