Profile image

Guilherme AlmeidaSoftware Engineer

Professional Software Development Blog and Portfolio.

"8 Puzzle" a game in Java

This is a sliding tile game created with Java to practice Object-Oriented Programming concepts and Design Patterns.

javaOOPdesign-patternsTDDMVCgame

Index


1 Intro

This is a project made for a college assignment in the subject of Object-Oriented Programming. The proposed challenge was to create the 8Puzzle (sliding tile game with eight pieces) in the Java language, applying the object-oriented methodologies to architect the software in an organized and reusable way. Also using technologies and libraries like Junit, JDBC and Swing. To create tests, serialization of the game state in a database and the graphical interface.

Source code: https://github.com/GuiSAlmeida/8puzzle-java

1.1 Goals

  • Apply Object-Oriented concepts taught in class to build software, such as:
    • Abstraction
    • Encapsulation
    • Composition
    • Inheritance
    • Polymorphism
  • Create test-oriented project TDD.
  • Keep the code clean without bad smells, with semantic naming of classes, methods and attributes.
  • Implement independent MVC layers.
  • Use Design Patterns.
  • Serialize game state in Postgres database.

1.2 How the game works

8Puzzle is a simple game consisting of a 3 x 3 board (containing 9 squares). One of the squares is empty (in my case use 0). The objective is to move to squares in different positions and have the numbers displayed in the correct sequence.
Move-in-a-game-of-8-puzzle


2 Tests (TDD)

Starting with the tests, using the framework Junit 5. They helped not only to maintain the quality and functionality of the code, but also to formulate how the board moves should happen.

Show me the code:
Test in the TestBoardControl class to simulate movement of the board.

package br.ies.aps.puzzle.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.Before;
import org.junit.Test;
import br.ies.aps.puzzle.model.Board;

public class TestBoardControl {
	private BoardControl boardControl;
	
	@Before
	public void config() {
		boardControl = new BoardControl(new Board());
	}

	/**
	* The test takes the position of the top field from the position of the pointer (empty field or 0)
	* and stores it in a variable to compare if when moving the pointer up
	* will be in the same position that was stored in the variable.
	* @link https://github.com/GuiSAlmeida/8puzzle-java/blob/master/test/br/ies/aps/puzzle/control/TestBoardControl.java
	*/
	@Test
	public void moveUpPointerTest() {
		Integer topPosition = boardControl.getBoard().getPointer().getTopField().getNumber();
		boardControl.moveUp();
		assertEquals(topPosition, boardControl.getBoard().getPointer().getBottomField().getNumber());
	}

	...
}

3 Independent Layers (MVC)

Defined the architecture of how the software elements will interact with each other.

3.1 Model

Here are the classes that represent the model of the system. The Model layer is isolated, containing the business rules and the classes that compose it cannot know ANYTHING about the external environment, that is, there must be no references to classes from other layers.

Show me the code:
Player class owns its private data and does not have access to data from other layers:

package br.ies.aps.puzzle.model;

public class Player {
	private String playerName;
	private Boolean winner = false;
	private Integer playerId;
	private Integer moves = 0;
	
	public Player(String name) {
		setPlayerName(name);
	}
	
	public String getPlayerName() {
		return playerName;
	}

	public void setPlayerName(String player) {
		this.playerName = player;
	}
	
	public Integer getMoves() {
		return moves;
	}	
	
	public void setMoves(Integer moves) {
		this.moves = moves;
	}	
	
	public Boolean getWinner() {
		return winner;
	}	
	
	public void setWinner(Boolean winner) {
		this.winner = winner;
	}	
	
	public Integer getPlayerId() {
		return playerId;
	}	
	
	public void setPlayerId(Integer playerId) {
		this.playerId = playerId;
	}
}

3.2 Controller

In the controller is the class responsible for transforming events generated by the interface, changing the model.

Show me the code:
BoardControl class connects interface to board state:

package br.ies.aps.puzzle.control;

import br.ies.aps.puzzle.model.Board;

public class BoardControl {
	private Board board;

	public BoardControl(Board board) {
		this.setBoard(board);
	}

	public Board getBoard() {
		return board;
	}

	public void setBoard(Board board) {
		this.board = board;
	}

	public void moveUp() {
		board.getPointer().moveUp();
		board.setPointer(board.getPointer().getTopField());
	}

	public void moveDown() {
		board.getPointer().moveDown();
		board.setPointer(board.getPointer().getBottomField());
	}

	public void moveLeft() {
		board.getPointer().moveLeft();
		board.setPointer(board.getPointer().getLeftField());
	}

	public void moveRight() {
		board.getPointer().moveRight();
		board.setPointer(board.getPointer().getRightField());
	}
}

3.3 View

User interface layer, where the user sees the state of the model and can manipulate the interface to activate business logic.

Show me the code:
ControlPanel class responsible for the interface of the controls part and receives user inputs:

package br.ies.aps.puzzle.view.swing.panel;

...

public class ControlPanel extends JPanel implements KeyListener, BoardObserver {
	private Board board;
	private Player player;
	private BoardPanel boardPanel;
	private JLabel movesLabel;
	private MoveUpButton topButton;
	private MoveDownButton bottomButton;
	private MoveRightButton rightButton;
	private MoveLeftButton leftButton;

	public ControlPanel(Board board, BoardPanel boardPanel, Player player) {
		this.board = board;
		this.boardPanel = boardPanel;
		this.player = player;
		generateBoardControl();
		this.board.registerObserver(this);
	}

	private void generateBoardControl() {
		GridBagLayout layout = new GridBagLayout();
		GridBagConstraints position = new GridBagConstraints();
		position.fill = GridBagConstraints.HORIZONTAL;

		setLayout(layout);

		position.gridy = 0;
		position.gridx = 0;
		JLabel playerLabel = new JLabel(String.format("Player: %s", player.getPlayerName()));
		add(playerLabel, position);

		position.gridy = 1;
		position.gridx = 0;
		position.gridwidth = 3;
		movesLabel = new JLabel(String.format("Moves: %d", player.getMoves()));
		add(movesLabel, position);

		position.gridwidth = 1;
		position.gridy = 0;
		position.gridx = 5;
		topButton = new MoveUpButton("↑", board, boardPanel, this, player);
		add(topButton, position);

		position.gridy = 2;
		position.gridx = 5;
		bottomButton = new MoveDownButton("↓", board, boardPanel, this, player);
		add(bottomButton, position);

		position.gridy = 1;
		position.gridx = 6;
		rightButton = new MoveRightButton("→", board, boardPanel, this, player);
		add(rightButton, position);

		position.gridy = 1;
		position.gridx = 4;
		leftButton = new MoveLeftButton("←", board, boardPanel, this, player);
		add(leftButton, position);
	}

	public void updateMoves(Integer number) {
		movesLabel.setText(String.format("Moves: %d", number));
	}

	public void endMoves(Integer number) {
		movesLabel.setText(String.format("Won the game with %d moves!!", number));
	}

	public void verifyEndGame(Board board) {
		if (board.verifyEndGame()) {
			endMoves(player.getMoves() + 1);
			player.setWinner(true);
			PlayerDAO playerDAO = new PlayerDAO(player);
			try {
				playerDAO.updateDatabase(player.getPlayerId());
			} catch (SQLException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} else {
			updateMoves(player.getMoves() + 1);
		}
	}

	@Override
	public void changeBoardState(Board board) {
		verifyEndGame(board);
	};

	@Override
	public void keyPressed(KeyEvent event) {
		Map<Integer, Runnable> map = new HashMap<Integer, Runnable>();

		map.put(KeyEvent.VK_DOWN, new Runnable() {
			@Override
			public void run() {
				bottomButton.changeBoardState();
			}
		});
		map.put(KeyEvent.VK_UP, new Runnable() {
			@Override
			public void run() {
				topButton.changeBoardState();
			}
		});
		map.put(KeyEvent.VK_RIGHT, new Runnable() {
			@Override
			public void run() {
				rightButton.changeBoardState();
			}
		});
		map.put(KeyEvent.VK_LEFT, new Runnable() {
			@Override
			public void run() {
				leftButton.changeBoardState();
			}
		});
		
		map.get(event.getKeyCode()).run();
	}
}

4 Saving state in the database (Postgres)

For the serialization of the players' data and the state of the board, a Postres database was created, which, through classes following DAO and Factory design patterns, connect the application to the database.

4.1 Conceptual Modeling

image

4.2 Physical modeling

CREATE DATABASE IF NOT EXISTS eight_puzzle;

CREATE TABLE player (
	id SERIAL PRIMARY KEY,
	name varchar(50),
	moves integer,
	winner boolean,
	board_id integer
);

CREATE TABLE BOARD (
	id SERIAL PRIMARY KEY,
	top_left_field integer,
	top_right_field integer,
	top_middle_field integer,
	middle_left_field integer,
	middle_field integer,
	middle_right_field integer,
	bottom_left_field integer,
	bottom_middle_field integer,
	bottom_right_field integer
);

ALTER TABLE player
	ADD CONSTRAINT board_id_fk 
	FOREIGN KEY (board_id)
	REFERENCES board (id)
	ON UPDATE CASCADE
	ON DELETE NO ACTION;

5 Design Patterns

Design Patterns are typical solutions to common problems in software design. They are like prefabricated blueprints that you can customize to solve a recurring design problem in your code.

5.1 Factory

The ConexionFactory class implements the Factory design pattern, which preaches the encapsulation of the construction (manufacturing) of complicated objects.

Show me the code:
The ConexionFactory class has the necessary data to create a database connection when instantiated:

package br.ies.aps.puzzle.model.DAO;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConexionFactory {	
	public static Connection getConnection() {
		try {
			final String url = "jdbc:postgresql://host:5432/database";
			final String user = "username";
			final String password  = "password";
			return DriverManager.getConnection(url, user, password);
			
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}
}

5.2 DAO (Data Access Object)

The DAO pattern is a design pattern that abstracts and encapsulates the data access mechanisms by hiding the execution details of the data source.

Show me the code:
PlayerDAO class responsible for inserting player data into the database:

package br.ies.aps.puzzle.model.DAO;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import br.ies.aps.puzzle.model.Player;

public class PlayerDAO {
	private Integer playerId;
	private String name;
	private Integer moves;
	private Boolean winner;

	public PlayerDAO(Player player) {
		this.name = player.getPlayerName();
		this.moves = player.getMoves();
		this.winner = player.getWinner();
	}

	public void insertDatabase(Integer boardId) throws SQLException {
		Connection conexao;
		String sql;
		ResultSet result;

		try {
			conexao = ConexionFactory.getConnection();
			sql = "INSERT INTO player (name, moves, winner, board_id) VALUES (?,?,?,?) RETURNING id;";
			PreparedStatement statement = conexao.prepareStatement(sql);
			statement.setString(1, name);
			statement.setInt(2, moves);
			statement.setBoolean(3, winner);
			statement.setInt(4, boardId);
			statement.execute();

			result = statement.getResultSet();
			result.next();

			playerId = result.getInt("id");
			result.close();
			statement.close();
			conexao.close();

		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}
	
	public void updateDatabase(Integer playerId) throws SQLException {
		Connection conexao;
		String sql;
		PreparedStatement statement;
		
		try {
			conexao = ConexionFactory.getConnection();
			sql = "UPDATE player SET moves = ?, winner = ? WHERE id = ?;";

			statement = conexao.prepareStatement(sql);
			statement.setInt(1, moves);
			statement.setBoolean(2, winner);
			statement.setInt(3, playerId);
			statement.execute();			
			statement.close();
			conexao.close();			
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	public Integer getPlayerId() {
		return playerId;
	}
}

5.3 Observer

Observer is a behavioral design pattern that allows you to define a subscription mechanism to notify multiple objects of any events that happen to the object they are observing.

Show me the code:
The Board class which owns the methods and core values of the game, is the publisher. A list is created to register the observerList and methods to add and notify them as soon as the state of the publisher changes.

package br.ies.aps.puzzle.model;

import java.util.ArrayList;
import java.util.List;

public class Board {
	...

	private List<BoardObserver> observerList = new ArrayList<>();

	public void registerObserver(BoardObserver observer) {
		observerList.add(observer);
	}

	public void notifyObservers(Board board) {
		for (BoardObserver observer : observerList) {
			observer.changeBoardState(this);
		}
	}

  ...
}

The BoardObserver interface declares the notification interface. It consists of a single update method (changeBoardState()) where the publisher (Board) passes its state at each update.

package br.ies.aps.puzzle.model;

public interface BoardObserver {
	public void changeBoardState(Board board);
}

Subscribers register as watchers and perform certain actions in response to notifications sent by the publisher. All these classes must implement the same interface (BoardObserver) so that the publisher is not attached to concrete classes. The class ControlPanel implements and overrides the method of BoardObserver and registers itself as an observer.

package br.ies.aps.puzzle.view.swing.panel;

...

public class ControlPanel extends JPanel implements KeyListener, BoardObserver {
	private Board board;
	private Player player;
	private BoardPanel boardPanel;
	private JLabel movesLabel;
	private MoveUpButton topButton;
	private MoveDownButton bottomButton;
	private MoveRightButton rightButton;
	private MoveLeftButton leftButton;

	public ControlPanel(Board board, BoardPanel boardPanel, Player player) {
		...

		this.board.registerObserver(this);
	}

	public void verifyEndGame(Board board) {
		if (board.verifyEndGame()) {
			endMoves(player.getMoves() + 1);
			player.setWinner(true);
			PlayerDAO playerDAO = new PlayerDAO(player);
			try {
				playerDAO.updateDatabase(player.getPlayerId());
			} catch (SQLException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} else {
			updateMoves(player.getMoves() + 1);
		}
	}

	@Override
	public void changeBoardState(Board board) {
		verifyEndGame(board);
	};

	...
}

6 Object Orientation

The paradigm of OOP (Object-Oriented Programming) is an analysis, design and programming model based on the approximation between the real world and the virtual world, through the creation and interaction between objects, attributes, codes, methods, among others. others.

6.1 Abstraction

In object orientation, a class is an abstraction of existing entities in the domain of the software system. An abstract class is designed to represent abstract entities and concepts. This class is always a superclass that has no instances. It defines a template for functionality and provides an incomplete implementation (the generic part of that functionality) that is shared by a group of derived classes (subclasses). Each of the derived classes completes the functionality of the abstract class by adding specific behavior.

Show me the code:
The ButtonMoviment class which is derived to the subclasses MoveUpButton, MoveDownButton, MoveLeftButton and MoveRightButton.

package br.ies.aps.puzzle.view.swing.button;

import java.awt.event.ActionListener;
import java.sql.SQLException;
import javax.swing.JButton;

import br.ies.aps.puzzle.control.BoardControl;
import br.ies.aps.puzzle.model.Player;
import br.ies.aps.puzzle.model.Board;
import br.ies.aps.puzzle.view.swing.panel.ControlPanel;
import br.ies.aps.puzzle.view.swing.panel.BoardPanel;

@SuppressWarnings("serial")
public abstract class ButtonMoviment extends JButton implements ActionListener {
	private Board board;
	private Player player;
	private BoardControl boardControl;
	private BoardPanel boardPanel;
	private ControlPanel controlBoard;

	public ButtonMoviment(String position, Board board, BoardPanel boardPanel,
		ControlPanel controlBoard, Player player) {
		setText(position);
		addActionListener(this);
		setBoard(board);
		setPlayer(player);
		setBoardControl(new BoardControl(board));
		setBoardPanel(boardPanel);
		setControleBoard(controlBoard);
	}

	public abstract void changeBoardState();

	public BoardControl getBoardControl() {
		return boardControl;
	}

	public void setBoardControl(BoardControl control) {
		this.boardControl = control;
	}

	public Board getBoard() {
		return board;
	}
	
	public Player getPlayer() {
		return player;
	}

	public void setPlayer(Player player) {
		this.player = player;
	}

	public void setBoard(Board board) {
		this.board = board;
	}

	public BoardPanel getBoardPanel() {
		return boardPanel;
	}

	public void setBoardPanel(BoardPanel boardPanel) {
		this.boardPanel = boardPanel;
	}

	public ControlPanel getControleBoard() {
		return controlBoard;
	}

	public void setControleBoard(ControlPanel controlBoard) {
		this.controlBoard = controlBoard;
	}
}

6.2 Encapsulation

Encapsulation is one of the pillars of object orientation, it serves to protect class data. Encapsulating an application's data means preventing them from being accessed improperly. For this, a structure is created where modifiers such as public, protected, private are used to restrict access to this data. And methods that can be used by any other class, without causing inconsistencies in the development commonly called getters and setters.

Show me the code:
The Field class which has encapsulated attributes and methods (getters and setters) for the data to be consulted.

package br.ies.aps.puzzle.model;

public class Field {
	private Field upField;
	private Field bottomField;
	private Field leftField;
	private Field rightField;  
	private Integer number;
	private Board board;

	public Field(Integer number, Board board) {
		this.setNumber(number);
		this.upField = this;
		this.bottomField = this;
		this.leftField = this;
		this.rightField = this;
		this.board = board;
	}

	public Integer getNumber() {
		return number;
	}

	public void setNumber(Integer number) {
		this.number = number;
	}

	public void changeNumber(Field origin, Field destino) {
		Integer temporario = origin.getNumber();
		origin.setNumber(destino.getNumber());
		destino.setNumber(temporario);
	}

	public Field getTopField() {
		return upField;
	}

	public void setTopField(Field upField) {
		this.upField = upField;
	}

	public Field getBottomField() {
		return bottomField;
	}

	public void setBottomField(Field bottomField) {
		this.bottomField = bottomField;
	}

	public Field getLeftField() {
		return leftField;
	}

	public void setLeftField(Field leftField) {
		this.leftField = leftField;
	}

	public Field getRightField() {
		return rightField;
	}

	public void setRightField(Field rightField) {
		rightField = rightField;
	}

	public void moveUp() {
		changeNumber(this, upField);
		this.board.notifyObservers(this.board);
	}

	public void moveDown() {
		changeNumber(this, bottomField);
		this.board.notifyObservers(this.board);
	}

	public void moveLeft() {
		changeNumber(this, leftField);
		this.board.notifyObservers(this.board);
	}

	public void moveRight() {
		changeNumber(this, rightField);
		this.board.notifyObservers(this.board);
	}
}

6.3 Composition

The main reason to use composition is that it allows you to reuse code without modeling an is-a association as you do by using inheritance. That allows stronger encapsulation and makes your code easier to maintain.

Show me the code:
The object created from the Board class instance will have objects of the Field class, in this case when the Board object is deleted, the Field objects will also be deleted.

package br.ies.aps.puzzle.model;

import java.util.ArrayList;
import java.util.List;

public class Board {
	private Field pointer;
	private Field middleField;
	private Field middleRightField;
	private Field middleLeftField;
	private Field middleBottomField;
	private Field bottomRightFiled;
	private Field bottomLeftField;
	private Field middleTopField;
	private Field topRightField;
	private Field topLeftField;

	private Integer boardId;

	private List<BoardObserver> observerList = new ArrayList<>();

	public Board() {
		gerarFields();
	}

	...

	public void gerarFields() {
		topLeftField = new Field(Integer.valueOf(7), this);
		middleTopField = new Field(Integer.valueOf(2), this);
		topRightField = new Field(Integer.valueOf(4), this);
		middleLeftField = new Field(Integer.valueOf(5), this);
		middleField = new Field(Integer.valueOf(0), this);
		middleRightField = new Field(Integer.valueOf(6), this);
		bottomLeftField = new Field(Integer.valueOf(8), this);
		middleBottomField = new Field(Integer.valueOf(3), this);
		bottomRightFiled = new Field(Integer.valueOf(1), this);

		linkNeighbors();
		setPointer(middleField);
	}

	...
}

6.4 Inheritance

An object can have methods and attributes from another class by inheritance, this means that the class has all the characteristics of the inherited class, in addition to being able to have its own as well. One of the great advantages of using inheritance is code reuse. This reuse can be triggered when it is identified that the attribute or method of a class will be the same for the others.

Show me the code:
There is inheritance in the class MoveDownButton which inherits the functionalities of the abstract class ButtonMoviment and overrides the method changeBoardState().

package br.ies.aps.puzzle.view.swing.button;

import java.awt.event.ActionEvent;
import br.ies.aps.puzzle.model.Player;
import br.ies.aps.puzzle.model.Board;
import br.ies.aps.puzzle.view.swing.panel.ControlPanel;
import br.ies.aps.puzzle.view.swing.panel.BoardPanel;

@SuppressWarnings("serial")
public class MoveDownButton extends ButtonMoviment {
	public MoveDownButton(String position, Board board, 
		BoardPanel boardPanel, ControlPanel controlBoard, Player player) {
		super(position, board, boardPanel, controlBoard, player);
	}

	@Override
	public void actionPerformed(ActionEvent event) {
		changeBoardState();
	}

	@Override
	public void changeBoardState() {
		this.getBoardControl().moveDown();
		this.getBoardPanel().updateBoardPanel(this.getBoard());
		Integer moves = this.getPlayer().getMoves();
		this.getPlayer().setMoves(moves + 1);
	}
}

6.5 Polymorphism

Term used to describe specific situations in which something can occur in different ways. There are two types of polymorphism:

The Static or Overload polymorphism occurs when we have the same operation implemented several times in the same class. The choice of which operation to call depends on the signature of the overloaded methods.

The Dynamic or Overlapping polymorphism, which is the principle that allows classes derived from the same superclass to have the same methods (with the same signature) but different behaviors. Same signature = same amount and type of parameters.

Show me the code: The class MoveLeftButton overwrites the method changeBoardState() inherited from the superclass ButtonMoviment according to its specificity, other classes that inherit from the same superclass will also have to implement the same method, but each one of a different way. Symbolizing the use of the polymorphism of overload.

package br.ies.aps.puzzle.view.swing.button;

import java.awt.event.ActionEvent;
import br.ies.aps.puzzle.model.Player;
import br.ies.aps.puzzle.model.Board;
import br.ies.aps.puzzle.view.swing.panel.ControlPanel;
import br.ies.aps.puzzle.view.swing.panel.BoardPanel;

@SuppressWarnings("serial")
public class MoveLeftButton extends ButtonMoviment {
	public MoveLeftButton(String position, Board board, BoardPanel boardPanel,
		ControlPanel controlBoard, Player player) {
		super(position, board, boardPanel, controlBoard, player);
	}

	@Override
	public void actionPerformed(ActionEvent event) {
		changeBoardState();
	}

	@Override
	public void changeBoardState() {
		this.getBoardControl().moveLeft();
		this.getBoardPanel().updateBoardPanel(this.getBoard());
		Integer moves = this.getPlayer().getMoves();
		this.getPlayer().setMoves(moves + 1);
	};
}

7 Playing

You need to have Java 8 or above version. Download the file 8puzzle.jar, open the terminal and issue the following command.

java -jar 8puzzle.jar

8 References

https://refactoring.guru/pt-br/design-patterns/factory-method
https://refactoring.guru/pt-br/design-patterns/observer
http://www.gqferreira.com.br/artigos/ver/mvc-com-java-desktop-parte1
http://www.dsc.ufcg.edu.br/~jacques/cursos/map/html/arqu/mvc/mvc.htm


9 Conclusion

This project helped me a lot to put into practice concepts of Object Orientation, as well as to exercise the logic of how software architecture works in a project using MVC and some Design Patterns. So, what did you think of this project? Do you have any suggestions or criticisms? Leave a reaction or a comment below. And thanks for visiting! 😉

Comments