
Everyone likes to play games, especially the simple ones. Tic tac toe is about as simple as you can get, and despite its age it is still widely played. In the past, the only way to play tic tac toe was on paper; now there are plenty of computerized options. In this article (inspired by our recent post, A Database Model for Simple Board Games), we’ll present a tutorial on building a tic tac toe web application with Spring Boot and AngularJS. The final code is hosted on GitHub.
Table of Contents
- User Authentication with Spring Security
- The Player Screen
- Playing the Game
- Retrieving the List of Game Moves
- Second Player Moves
Introduction
We’ll begin with a gentle introduction to the development of our app, starting with a basic overview of the game’s functionality. The final application will consist of three screens, one of which is automatically generated by the Spring Security framework.
The custom login page, automatically rendered by Spring Security:
The screen where players can create a new game, join an existing game, or load a previous game:
The game screen:
The Technology Stack
These are the technologies we will use to build our app:
- Spring Boot – A Spring-based application, ready to “run” with no additional configuration
- Spring Data JPA – A Spring extension that allows the creation of repositories based on Spring and JPA
- AngularJS – A JavaScript MVC framework
- Bootstrap – A CSS framework
Dependencies
This game will use several dependencies. They are added to the classpath
with Gradle.
To see them all, please check the dependencies closure in build.gradle
; here’s a partial list:
dependencies { compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-security") compile("org.postgresql:postgresql:9.4-1206-jdbc42") compile("com.h2database:h2:1.4.192") compile("org.projectlombok:lombok:1.16.8") compile("com.google.guava:guava:16.0.1") testCompile("junit:junit") }
The Database Model
Let’s first inspect the database behind the game. Below, you will find the model that the application will use:
For the application’s purposes, we need three tables. The game
table stores game details, the move
table contains a list of all player moves for each particular game, and the player
table stores all the player-specific data.
The Game Table
The game
table contains these columns:
first_player_id
: references thePlayer
table. Its value relates to the player that created the game.second_player_id
: references the second player in the game. This value can be nullable, because the second player may be the application (“COMPUTER”).created
: contains the date and time when the game was created.game_status
: contains possible game statuses:WAITS_FOR_PLAYER
,IN_PROGRESS
,FIRST_PLAYER_WON
,SECOND_PLAYER_WON
,TIE
. Its value is checked by the database in aCHECK
constraint.game_type
: contains two possible game types:COMPUTER
orCOMPETITION
. This value is also checked by the database in aCHECK
constraint.first_player_piece_code
: stores the values ‘X’ or ‘O’. This value too is checked by aCHECK
constraint.
Object-Relational Mapping with Hibernate
We’re using Hibernate for object-relational mapping. Hibernate follows the “object-first” approach. It means that the appropriate database structures are generated based on the Java code.
Here is the Game
class, which is mapped to the game
table. Notice that no constructors or getters and setters are written explicitly. Rather, they are generated. We’re using the Lombok library for this.
As you see, the Game
class is annotated by @Getter
and @Setter
. They are responsible for generating getters and setters for each field in the class. The @NoArgsConstructor
and @AllArgsConstructor
annotators are responsible for generating the default and all-args constructors respectively.
A Hibernate Crib Sheet:
@Entity | Marks a class as an entity bean |
@Id | Marks a field as a primary key |
@GeneratedValue | Defines a primary key generation strategy |
@Column | Specifies the details of the column to which the field will be mapped |
@ManyToOne | Maps a many-to-one relationship |
@JoinColumn | Indicates the entity is the owner of the relationship (The corresponding table has a column with a foreign key to the referenced table.) |
@Enumerated | Converts database data to and from Java enum types |
@Check | Defines the optional check constraint based on the SQL statement |
Here’s the code:
@Entity @Getter @Setter @Check(constraints = "first_player_piece_code = 'O' or first_player_piece_code = 'X' " + "and game_type = 'COMPUTER' or game_type = 'COMPETITION' " + "and game_status = 'IN_PROGRESS' or game_status = 'FIRST_PLAYER_WON' or game_status = 'SECOND_PLAYER_WON'" + "or game_status = 'TIE' or game_status = 'WAITS_FOR_PLAYER' ") @NoArgsConstructor @AllArgsConstructor public class Game { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id", nullable = false) private Long id; @ManyToOne @JoinColumn(name = "second_player_id", nullable = true) private Player secondPlayer; @ManyToOne @JoinColumn(name = "first_player_id", nullable = false) private Player firstPlayer; @Enumerated(EnumType.STRING) private Piece firstPlayerPieceCode; @Enumerated(EnumType.STRING) private GameType gameType; @Enumerated(EnumType.STRING) private GameStatus gameStatus; @Column(name = "created", nullable = false) private Date created;
The Move Table
The move
table will store all information related to players’ moves. Looking at this table, we can see who made what move, in which game, at what time, and in which cell.
The Java entity that will generate this table is very similar to the one used for the Game entity. We’ve omitted it here for the sake of clarity. To check it, please visit the app repository on GitHub.
The Player Table
Finally, take a look at the player
table. It stores player-specific data: each player’s user_name
, password_hash
, and email
.
Once again, we’ve omitted the Java entity here for clarity’s sake; it’s quite like the Game one, too. To check it, please visit the app repository on GitHub.
The Technology
Spring Boot
Because this application consists of many elements, I want to take a moment to introduce Spring Boot’s content. Let’s start with the packages. When you take a look at the application’s code on GitHub, you will see that there are many separated layers. This is also shown below:
Let’s briefly list what’s going on in here. Most of it is self-explanatory:
- The
config
package contains the HTTP Security configuration. - The
controller
package contains classes that handle HTTP requests. - The
domain
package contains the entity classes. - The
DTO
package contains the classes used to map request parameters. - The
enums
package contains the enums used in entity classes and that are mapped to appropriate columns in thegame
table. In this case, the enums are GameStatus (possible game statuses), GameType (possible game types) and Piece (pieces available in the game). - The
repository
package contains the repository interfaces that provide CRUD functionality to the entity classes. - The
security
package contains the classes responsible for user authentication. - The
service
package identifies the application layer that encapsulates the business logic, controls transactions, etc.
AngularJS
The Angular application content is located in the webapp
package:
The Angular application’s main configuration is placed in the application.js
file. As you can see below, the first line of this file is the ticTacToe module declaration, which specifies how the application should be bootstrapped. (The dependency modules are in the square brackets). After that, the routing config is specified: this divides the application into logical views and binds the different views to controllers.
var ticTacToe = angular.module('ticTacToe', ['ngRoute', 'gameModule']); ticTacToe.config(['$routeProvider', function($routeProvider) { $routeProvider. when('/about', { templateUrl: 'templates/about.html' }). when('/game/:id', { templateUrl: 'templates/game-board.html', controller: 'gameController' }). when('/player/panel', { templateUrl: 'templates/player-panel.html', controller: 'newGameController' }). otherwise({ redirectTo: '/player/panel' }); }]);
The above code defines three urls that are mapped by views: templates/about.html
, templates/game-board.html
and templates/player-panel.html
. For example, when we open http://localhost:8080/#/player/panel in the browser, Angular automatically matches it with the route we configured and loads the templates/player-panel.html
template. It then invokes newGameController
, where the logic for the view is added.
Implementation
When you take a look at the GitHub code, you will definitely agree that the application details create a lot of room for discussion – more than we can fit for here. That’s why we’re focusing only on particular elements. When describing the selected functionality, we will start with what we see on the screen and end with the database.
User Authentication with Spring Security
Whenever we start the game app, we will be redirected to an automatically-generated login form. I decided not to worry about user authentication and leave it to Spring Security. Consequently, the Spring application will present a login form to the player, check whether each request is carried out by an authenticated user, etc. Before we move to the Java code, have a look at what the login screen looks like:
We’ve already placed Spring Security in the dependencies section in build.gradle
.We’ll now examine our configuration – specifically, the code for two classes: ContextUser
and UserDetailsServiceImpl
.
Let’s start with the ContextUser
class. Here we construct the user object by extending the User
class from Spring Security with the details required by DaoAuthenticationProvider()
. In this case, the credentials will be the player’s username and password. If the user provides a correct username and password combination, they will be granted “create” authority. Other parameters in the constructor are set to true; we are not concerned if the account has expired or if the app is locked because it’s being used as a demo.
public class ContextUser extends org.springframework.security.core.userdetails.User { private final Player player; public ContextUser(Player player) { super(player.getUserName(), player.getPassword(), true, true, true, true, ImmutableSet.of(new SimpleGrantedAuthority("create"))); this.player = player; } public Player getPlayer() { return player; } }
Let’s go further and customize the security settings. This time, take a look at the UserDetailsServiceImpl
class.
@Component public class UserDetailsServiceImpl implements UserDetailsService { private final PlayerRepository playerRepository; @Autowired public UserDetailsServiceImpl(PlayerRepository playerRepository) { this.playerRepository = playerRepository; } @Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { checkNotNull(username); if(isEmpty(username)) { throw new UsernameNotFoundException("Username cannot be empty"); } Player player = playerRepository.findOneByUserName(username); if (player == null) { throw new UsernameNotFoundException("Player " + username + " doesn't exists"); } return new ContextUser(player); } }
Next, let’s consider the SecurityConfig
class, which you can find in the config
package. This class ensures that only authenticated users can access application pages. The SecurityConfig
class is annotated with @EnableWebSecurity
to enable Spring Security’s web security support. The class also extends WebSecurityConfigurerAdapter
and overrides a couple of methods to set some specifics for web security configuration.
Now have a look at configure(HttpSecurity http)
. We will override this method to configure Spring’s HttpSecurity. This class tells Spring:
- that any requested URL will require an authenticated user.
- the specifics of the form-support-based authentication, which will generate the default login page.
The interface UserDetailsService
loads user-specific data. It is employed throughout the framework as a user DAO.
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private PlayerRepository playerRepository; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(new UserDetailsServiceImpl(playerRepository)) .passwordEncoder(new BCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .usernameParameter("username") .passwordParameter("password") .and() .httpBasic() .and() .csrf().disable(); }
The Player Screen
We now are done with user authentication and can move on to the actual game. After logging in, the player is redirected to the /player/panel
endpoint. In this page, they can:
- Create a new game and play against an AI opponent (the ‘COMPUTER’).
- Create a new game and play against a human.
- Join a two-player game.
- Resume a game.
Ad1 and Ad2 : Creating a New Game
From the drop-down menu, the user can choose one of two Game types. The ‘COMPUTER’ value will create a game with the ‘COMPUTER’ type, in which the application is the second player. The second type is ‘COMPETITION’, in which two human opponents play against each other.
The next field we have is Play as. This lets the player choose which piece they will use ( X or O). After clicking the New Game button, the selected values are sent as a JSON object in the POST request to the /game/create
endpoint.
The appropriate sequence of actions are visualized in the diagram below.
Let’s pick up the action with moving to the Angular application. In gameModule.js, we have the controller (newGameController
) where the logic for creating a new game is located. In particular, please look at the createNewGame()
function. It is created in the scope (an object containing model data). A scope joins the controller with the views.
The above-mentioned POST request is made by Angular. Below is the function that sends the POST request:
scope.createNewGame = function () { var data = scope.newGameData; var params = JSON.stringify(data); http.post("/game/create", params, { headers: { 'Content-Type': 'application/json; charset=UTF-8' } }).success(function (data, status, headers, config) { rootScope.gameId = data.id; location.path('/game/' + rootScope.gameId); }).error(function (data, status, headers, config) { location.path('/player/panel'); }); }
Now, let’s move to the Spring Boot GameController
class that will handle this request. In this case, createNewGame()
is responsible. Here’s the code:
@RequestMapping(value = "/create") public Game createNewGame(@RequestBody GameDTO gameDTO) { Game game = gameService.createNewGame(playerService.getLoggedUser(), gameDTO); httpSession.setAttribute("gameId", game.getId()); return game; }
The value placed in the @RequestMapping
annotation maps the web request to the appropriate function. The request body will be mapped to GameDTO
by the @RequestBody
annotation. GameDTO
can appropriately request two fields: GameType
and Piece
.
For creating the object, we use the createNewGame()
function from the service package. This takes two arguments: the logged user object, and the gameDTO object. After creating the game, a game ID is saved in the session.
public Game createNewGame(Player player, GameDTO gameDTO) { Game game = new Game(); game.setFirstPlayer(player); game.setGameType(gameDTO.getGameType()); game.setFirstPlayerPieceCode(gameDTO.getPiece()); game.setGameStatus(gameDTO.getGameType() == GameType.COMPUTER ? GameStatus.IN_PROGRESS : GameStatus.WAITS_FOR_PLAYER); game.setCreated(new Date()); gameRepository.save(game); return game; }
In the response, the Angular application receives the new game’s object and redirects the user to a new game page.
Ad3. Join a Two-Player Game
At the right side of the screen, we can see games created by other users. To get the list of those games,
Angular sends a GET request to the /game/list
endpoint. The user can join a game by clicking the Join button
Once again, let’s take a look at Angular’s role. The Angular application sends the HTTP GET request to the /game/list
endpoint. The appropriate code is below.
http.get('/game/list').success(function (data) { scope.gamesToJoin = data; }).error(function (data, status, headers, config) { location.path('/player/panel'); });
The Spring Boot function that handles the /game/list
endpoint returns a list of games. This is what the code looks like:
@RequestMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE) public ListgetGamesToJoin() { return gameService.getGamesToJoin(playerService.getLoggedUser()); }
Let’s now take a look at the GameService
class:
public ListgetGamesToJoin(Player player) { return gameRepository.findByGameTypeAndGameStatus(GameType.COMPETITION, GameStatus.WAITS_FOR_PLAYER).stream().filter(game -> game.getFirstPlayer() != player).collect(Collectors.toList()); }
The function findByGameTypeAndGameStatus()
returns a list of games that are of the COMPETITION
type and have the WAITS_FOR_PLAYER
status. Unfortunately, this list also contains games created by the current user. So each of the games is filtered in the stream and the appropriate objects collected in a separate list. If the object of firstPlayer
in the processed game object differs from the object of the currently logged player, that game is added to the list.
Ad4. Resume a Game
As with the previous action, Angular again sends a request for the player’s games:
http.get('/game/player/list').success(function (data) { scope.playerGames = data; }).error(function (data, status, headers, config) { location.path('/player/panel'); });
The Java function that handles this request is as follows:
@RequestMapping(value = "/player/list", produces = MediaType.APPLICATION_JSON_VALUE) public ListgetPlayerGames() { return gameService.getPlayerGames(playerService.getLoggedUser()); }
The getPlayerGames()
function takes only one argument, which is the object of the currently logged user. This is retrieved from the session. Let’s move to game services and look at this function:
public ListgetPlayerGames(Player player) { return gameRepository.findByGameStatus( GameStatus.IN_PROGRESS).stream().filter(game -> game.getFirstPlayer() == player).collect(Collectors.toList()); }
The function findByGameStatus()
returns games with the status IN_PROGRESS
. The result contains all users’ games, filtered by the player’s object. If the firstPlayer
object of the game is the same as the current player, the game is added to the list. The list of filtered games is returned to Angular and displayed to the user.
Playing the Game
After a new game is created, the user sees a fresh page. On the left side, there is an empty board; the right side is dedicated to moves performed during the game.
The board is generated from a defined array of rows. Each row and cell has a CSS class set.
Below is the HTML that corresponds to the board you see above. The individual cells are generated from the rows array: