Building a scalable Javascript mobile app backend solution with Horizon

Building a scalable Javascript mobile app backend solution with Horizon

【51CTO.com Quick Translation】

Introduction

Horizon is a well-known cross-platform scalable backend framework suitable for building cross-platform JavaScript-based mobile applications, especially those that require real-time capabilities. This framework was developed by programmers from the RethinkDB product, so it uses RethinkDB as the default database. If you are not familiar with RethinkDB, then you only need to know that it is an open source database that supports real-time functions (https://www.rethinkdb.com).

The Horizon framework exposes a set of client APIs to allow you to interact with the underlying database. This means that you don't have to write any backend code. All you have to do is build a new server, run it, and Horizon will automatically manage the rest. With Horizon, you can easily synchronize data between real-time connected clients and servers.

If you want to learn more about Horizon, please check out its FAQ page (http://horizon.io/faq/).

In this tutorial, you will use Icon and Horizon to develop a Tic-Tac-Toe game. Therefore, the premise of reading this article is that you already know Icon and Horizon, so I am not going to explain the specific code related to Icon in the program. Of course, if you want some background knowledge about Icon, I suggest you check this website http://ionicframework.com/getting-started/. If you want to continue reading this article, please download the sample project source code of the article first (https://github.com/anchetaWern/ionic-horizon-tictactoe).

The following figure shows a snapshot of the final result of this article's sample application.

Install Horizon

RethinkDB is used as the database for Horizon. Therefore, you need to install RethinkDB before installing Horizon. For detailed information about installing RethinkDB, you can find the answer from the URL https://www.rethinkdb.com/docs/install/.

Once RethinkDB is installed, you can install Horizon using npm by executing the following command in your terminal:

npm install -g horizon

Horizon Server Development

The Horizon server is used as the backend for the application. Whenever the application executes code, it communicates with the database.

You can create a new Horizon server by executing the following command in your terminal:

hz init tictactoe-server

This command will create the RethinkDB database and provide the files used by Horizon.

Once the server is created, you can run it by executing:

hz serve --dev

In the above command, you specify -dev as an option. This means that you want to run a development server. The following options are set in the development server:

--secure no: This means that websockets and files will not be served over an encrypted connection.

--permissions no: Disables permission constraints. This means that any client can perform any operation they want in the database. Horizon's permission system is based on whitelists. This means that, by default, all users have no permission to do anything. You must explicitly specify which operations are allowed.

--auto-create-collection yes: Automatically create a collection when it is first used. In Horizon, a collection is equivalent to a table in a relational database. Setting this option to true means that every time a client uses a new collection, it will be automatically created.

--auto-create-index yes: Automatically create index on first use.

--start-rethinkdb yes: Automatically start a new instance of RethinkDB in the current directory.

--allow-unauthenticated yes: Allow unauthenticated users to perform database operations.

--allow-anonymous yes: Allow anonymous users to perform database operations.

--serve-static ./dist: Enables serving of static files. This is useful if you want to test interacting with the Horizon API in a browser. The Horizon server runs on port 8181 by default, so you can access the server by visiting http://localhost:8181.

Note: The --dev option should never be used in a production environment, as it opens a large number of vulnerabilities that can be easily exploited by attackers.

Building an Ionic App

Now, we are fully prepared. Next, we will create an Ionic application framework with the following command:

ionic start tictactoe blank

Install Chance.js

Next, you need to install chance.js, which is a JavaScript utility library for generating random data. In this application, we use it to generate a unique ID for the player. You can install chance.js through the bower tool using the following command:

bower install chance

Create index.html

Now, open the file www/index.html and modify its content as follows:

  1. <!DOCTYPE html>
  2.  
  3. <html>
  4.  
  5. <head>
  6.  
  7. <meta charset= "utf-8" >
  8.  
  9. <meta name = "viewport" content= "initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width" >
  10.  
  11. <title></title>
  12.  
  13. <link href= "lib/ionic/css/ionic.css" rel= "stylesheet" >
  14.  
  15. <link href= "css/style.css" rel= "stylesheet" >
  16.  
  17. <! -- IF using Sass (run gulp sass first), then uncomment below and remove the CSS includes above  
  18.  
  19. <link href= "css/ionic.app.css" rel= "stylesheet" >
  20.  
  21. -->  
  22.  
  23. <! -- chance.js -->  
  24.  
  25. <script src= "lib/chance/dist/chance.min.js" ></script>
  26.  
  27. <! -- ionic/angularjs js -->  
  28.  
  29. <script src= "lib/ionic/js/ionic.bundle.js" ></script>
  30.  
  31. <! -- cordova script (this will be a 404 during development) -->  
  32.  
  33. <script src= "cordova.js" ></script>
  34.  
  35. <! -- horizon script -->  
  36.  
  37. <script src= "http://127.0.0.1:8181/horizon/horizon.js" ></script>
  38.  
  39. <! -- your app's js -->  
  40.  
  41. <script src= "js/app.js" ></script>
  42.  
  43. <! --main app logic -->  
  44.  
  45. <script src= "js/controllers/HomeController.js" ></script>
  46.  
  47. </head>
  48.  
  49. <body ng-app= "starter" >
  50.  
  51. <ion-nav- view ></ion-nav- view >
  52.  
  53. </body>
  54.  
  55. </html>

Most of the code above comes from the boilerplate code generated by the Icon Blank Wizard template. Now, let's add a reference to the chance.js script:

  1. <script src= "lib/chance/dist/chance.min.js" ></script>

The Horizon server will automatically provide the Horizon script service. The code is as follows:

  1. <script src= "http://127.0.0.1:8181/horizon/horizon.js" ></script>

[Note] If you want to deploy this content later, you must modify the URL.

Next, the main application logic is located in the following script file:

  1. <script src= "js/controllers/HomeController.js" ></script>

Write the main program app.js

The file app.js is where the code that initializes the application is run. Next, you need to open the file www/js/app.js and add the following content below the run function:

  1. .config( function ($stateProvider, $urlRouterProvider) {
  2.  
  3. $stateProvider
  4.  
  5. .state( 'home' , {
  6.  
  7. cache: false ,
  8.  
  9. url: '/home' ,
  10.  
  11. templateUrl: 'templates/home.html'  
  12.  
  13. });
  14.  
  15. // if none of the above states are matched, use this as the fallback
  16.  
  17. $urlRouterProvider.otherwise( '/home' );
  18.  
  19. });

This will set up a route for the default application page. This route will specify the template to use for the page and the URL where it can be accessed.

Develop the controller program HomeController.Js

Now, we create a controller file HomeController.js in the path www/js/controllers and modify its code as follows:

  1. ( function () {
  2.  
  3. angular.module( 'starter' )
  4.  
  5. .controller( 'HomeController' , [ '$scope' , HomeController]);
  6.  
  7. function HomeController($scope){
  8.  
  9. var me = this;
  10.  
  11. $scope.has_joined = false ;
  12.  
  13. $scope.ready = false ;
  14.  
  15. const horizon = Horizon({host: 'localhost:8181' });
  16.  
  17. horizon.onReady( function (){
  18.  
  19. $scope.$apply( function () {
  20.  
  21. $scope.ready = true ;
  22.  
  23. });
  24.  
  25. });
  26.  
  27. horizon.connect ();
  28.  
  29. $ scope.join = function (username, room){
  30.  
  31. me.room = horizon( 'tictactoe' );
  32.  
  33. var id = chance. integer ({ min : 10000, max : 999999});
  34.  
  35. me.id = id;
  36.  
  37. $scope.player = username;
  38.  
  39. $scope.player_score = 0;
  40.  
  41. me.room.findAll({room: room, type: 'user' }). fetch ().subscribe( function (row){
  42.  
  43. var user_count = row.length;
  44.  
  45. if(user_count == 2){
  46.  
  47. alert( 'Sorry, room is already full.' );
  48.  
  49. } else {
  50.  
  51. me.piece = 'X' ;
  52.  
  53. if(user_count == 1){
  54.  
  55. me.piece = 'O' ;
  56.  
  57. }
  58.  
  59. me.room.store({
  60.  
  61. id: id,
  62.  
  63. room: room,
  64.  
  65. type: 'user' ,
  66.  
  67. name : username,
  68.  
  69. piece: me.piece
  70.  
  71. });
  72.  
  73. $scope.has_joined = true ;
  74.  
  75. me.room.findAll({room: room, type: 'user' }).watch().subscribe(
  76.  
  77. function (users){
  78.  
  79. users.forEach( function ( user ){
  80.  
  81. if( user .id != me.id ){
  82.  
  83. $scope.$apply( function () {
  84.  
  85. $scope.opponent = user . name ;
  86.  
  87. $scope.opponent_piece = user .piece;
  88.  
  89. $scope.opponent_score = 0;
  90.  
  91. });
  92.  
  93. }
  94.  
  95. });
  96.  
  97. },
  98.  
  99. function (err){
  100.  
  101. console.log(err);
  102.  
  103. }
  104.  
  105. );
  106.  
  107. me.room.findAll({room: room, type: 'move' }).watch().subscribe(
  108.  
  109. function (moves){
  110.  
  111. moves.forEach( function (item){
  112.  
  113. var block = document.getElementById(item.block);
  114.  
  115. block.innerHTML = item.piece;
  116.  
  117. block.className = "col done" ;
  118.  
  119. });
  120.  
  121. me.updateScores();
  122.  
  123. },
  124.  
  125. function (err){
  126.  
  127. console.log(err);
  128.  
  129. }
  130.  
  131. );
  132.  
  133. }
  134.  
  135. });
  136.  
  137. }
  138.  
  139. $scope.placePiece = function (id){
  140.  
  141. var block = document.getElementById(id);
  142.  
  143. if(!angular.element(block).hasClass( 'done' )){
  144.  
  145. me.room.store({
  146.  
  147. type: 'move' ,
  148.  
  149. room: me.room_name,
  150.  
  151. block: id,
  152.  
  153. piece: me.piece
  154.  
  155. });
  156.  
  157. }
  158.  
  159. };
  160.  
  161. me.updateScores = function (){
  162.  
  163. const possible_combinations = [
  164.  
  165. [1, 4, 7],
  166.  
  167. [2, 5, 8],
  168.  
  169. [3, 2, 1],
  170.  
  171. [4, 5, 6],
  172.  
  173. [3, 6, 9],
  174.  
  175. [7, 8, 9],
  176.  
  177. [1, 5, 9],
  178.  
  179. [3, 5, 7]
  180.  
  181. ];
  182.  
  183. var scores = { 'X' : 0, 'O' : 0};
  184.  
  185. possible_combinations.forEach( function (row, row_index){
  186.  
  187. var pieces = { 'X' : 0, 'O' : 0};
  188.  
  189. row.forEach( function (id, item_index){
  190.  
  191. var block = document.getElementById(id);
  192.  
  193. if(angular.element(block).hasClass( 'done' )){
  194.  
  195. var piece = block.innerHTML;
  196.  
  197. pieces[piece] += 1;
  198.  
  199. }
  200.  
  201. });
  202.  
  203. if(pieces[ 'X' ] == 3){
  204.  
  205. scores[ 'X' ] += 1;
  206.  
  207. } else if(pieces[ 'O' ] == 3){
  208.  
  209. scores[ 'O' ] += 1;
  210.  
  211. }
  212.  
  213. });
  214.  
  215. $scope.$apply( function () {
  216.  
  217. $scope.player_score = scores[me.piece];
  218.  
  219. $scope.opponent_score = scores[$scope.opponent_piece];
  220.  
  221. });
  222.  
  223. }
  224.  
  225. }
  226.  
  227. })();

Now, let's analyze the above code. First, the default state is set. Among them, the has_joined variable is used to determine whether the player has entered a room. Secondly, the ready variable is used to determine whether the user has connected to the Horizon server. When this variable value is false, the application interface cannot be displayed to the user.

  1. $scope.has_joined = false ;
  2.  
  3. $scope.ready = false ;

The code to connect to the server is as follows:

  1. const horizon = Horizon({host: 'localhost:8181' });
  2.  
  3. horizon.onReady( function (){
  4.  
  5. $scope.$apply( function () {
  6.  
  7. $scope.ready = true ;
  8.  
  9. });
  10.  
  11. });
  12.  
  13. horizon.connect (); // connect   to the server

As I mentioned earlier, the Horizon server uses port 8181 by default. That's why we use local:8181 as the port. If you are connecting to a remote server, this should correspond to the IP address or domain name assigned to the server. When a user connects to the server, the onReady event will be fired. It is at this point that we set ready to true so that we can show the UI portion of the application to the user.

  1. horizon.onReady( function (){
  2.  
  3. $scope.$apply( function () {
  4.  
  5. $scope.ready = true ;
  6.  
  7. });
  8.  
  9. });

Enter the room

Whenever the user clicks the Join button, the join function will be executed:

  1. $ scope.join = function (username, room){
  2.  
  3. ...
  4.  
  5. };

Inside this function, connect to a collection called tictactoe.

【Note】Because we are in development mode; therefore, if the collection does not exist, the system will automatically create it for you.

  1. me.room = horizon( 'tictactoe' );

Next, generate an ID and set it as the current user's ID:

  1. var id = chance. integer ({ min : 10000, max : 999999});
  2.  
  3. me.id = id;

Next, set the player username and default player score.

  1. $scope.player = username;
  2.  
  3. $scope.player_score = 0;

Note: Both variables are bound to the template; therefore, you can display and update them at any time.

Next, we query the document with the condition that the room attribute is the current room and the type attribute is user. Do not confuse this query with the subscribe function, as we are not monitoring data changes here. Moreover, we use the fetch function here; this means that the operation is performed only when the user enters a room. The relevant code is as follows:

  1. me.room.findAll({room: room, type: 'user' }). fetch ().subscribe( function (row){
  2.  
  3. ...
  4.  
  5. });

Once the result comes back, the number of users is checked. Of course, TicTacToe can only be played by two players, so if a user tries to join a room that already has two players, they will be alerted.

  1. var user_count = row.length;
  2.  
  3. if(user_count == 2){
  4.  
  5. alert( 'Sorry, room is already full.' );
  6.  
  7. } else {
  8.  
  9. ...
  10.  
  11. }

The else statement in the code above will continue the logic of accepting users, which determines the cards that will be assigned to users based on the current number of users. The first person to join the room gets the "X" card, while the second person gets the "O" card.

  1. me.piece = 'X' ;
  2.  
  3. if(user_count == 1){
  4.  
  5. me.piece = 'O' ;
  6.  
  7. }

Once you select a card, store the new user in the collection and invert the has_joined switch to display the tic-tac-toe board.

  1. me.room.store({
  2.  
  3. id: id,
  4.  
  5. room: room,
  6.  
  7. type: 'user' ,
  8.  
  9. name : username,
  10.  
  11. piece: me.piece
  12.  
  13. });
  14.  
  15. $scope.has_joined = true ;

Next, listen for changes in the collection. This time, instead of fetching, use a watch method. Specifically, the callback function is executed every time a new document is added or an existing document matching the query is updated (or deleted). When the callback function executes, it loops through all the results and sets the opponent's details - if the user ID of the document does not match the current user's ID. This is how we use it in this program to show the current user who their opponent is.

  1. me.room.findAll({room: room, type: 'user' }).watch().subscribe(
  2.  
  3. function (users){
  4.  
  5. users.forEach( function ( user ){
  6.  
  7. if( user .id != me.id ){
  8.  
  9. $scope.$apply( function () {
  10.  
  11. $scope.opponent = user . name ;
  12.  
  13. $scope.opponent_piece = user .piece;
  14.  
  15. $scope.opponent_score = 0;
  16.  
  17. });
  18.  
  19. }
  20.  
  21. });
  22.  
  23. },
  24.  
  25. function (err){
  26.  
  27. console.log(err);
  28.  
  29. }
  30.  
  31. );

Next we subscribe to the move event, which fires every time a player places their card on the board, which causes the document to change. If this happens, we loop through all the moves and add the text to the corresponding cells. From now on, the code will use the word "block" to refer to each cell on the board.

The text added corresponds to the card each user has played; in addition, the class name is replaced with "col done" in the code. Among them, col corresponds to the grid implementation class in Ionic programming, and done is a class used to indicate that a card has already been placed on a specific block. We use this method to check if the user can still place cards on the grid. After updating the chessboard UI, the player's score is updated by calling the updateScores function (this function will be added later).

  1. me.room.findAll({room: room, type: 'move' }).watch().subscribe(
  2.  
  3. function (moves){
  4.  
  5. moves.forEach( function (item){
  6.  
  7. var block = document.getElementById(item.block);
  8.  
  9. block.innerHTML = item.piece;
  10.  
  11. block.className = "col done" ;
  12.  
  13. });
  14.  
  15. me.updateScores();
  16.  
  17. },
  18.  
  19. function (err){
  20.  
  21. console.log(err);
  22.  
  23. }
  24.  
  25. );

Place Card

Whenever the user clicks on a square on the board, the placePiece function is called, and the ID value of the corresponding square is provided as a parameter to this function. This allows us to manipulate the game squares as we like. In this program, this function is used to check whether a game square is of the done type. If the done flag is not set, a new move is created and the current room, square ID value and corresponding card are displayed.

  1. $scope.placePiece = function (id){
  2.  
  3. var block = document.getElementById(id);
  4.  
  5. if(!angular.element(block).hasClass( 'done' )){
  6.  
  7. me.room.store({
  8.  
  9. type: 'move' ,
  10.  
  11. room: me.room_name,
  12.  
  13. block: id,
  14.  
  15. piece: me.piece
  16.  
  17. });
  18.  
  19. }
  20.  
  21. };

Update player score

To update the player's score, you need to build an array containing the possible winning combinations, like this:

  1. const possible_combinations = [
  2.  
  3. [1, 4, 7],
  4.  
  5. [2, 5, 8],
  6.  
  7. [3, 2, 1],
  8.  
  9. [4, 5, 6],
  10.  
  11. [3, 6, 9],
  12.  
  13. [7, 8, 9],
  14.  
  15. [1, 5, 9],
  16.  
  17. [3, 5, 7]
  18.  
  19. ];

In this code, [1, 4, 7] corresponds to the first row and [1, 2, 3] corresponds to the first column. The order does not matter as long as the corresponding numbers exist. The following graphic will help you understand this more intuitively.

Next, you need to initialize the score of each individual card and loop through each possible combination. For each loop iteration, initialize the total number of cards that have been placed on the board. Then loop through each possible combination. Use the id to check if the corresponding grid already has a card. If so, get the actual card and add 1 to the total number of cards. After the loop is finished, check if the total number of cards is equal to 3. If the total number of cards is equal to 3, increase the score of the card until all possible combinations are iterated through. Once completed, update the score values ​​of the current player and the opponent.

  1. var scores = { 'X' : 0, 'O' : 0};
  2.  
  3. possible_combinations.forEach( function (row, row_index){
  4.  
  5. var pieces = { 'X' : 0, 'O' : 0};
  6.  
  7. row.forEach( function (id, item_index){
  8.  
  9. var block = document.getElementById(id);
  10.  
  11. if(angular.element(block).hasClass( 'done' )){ // check if there's already a piece
  12.  
  13. var piece = block.innerHTML;
  14.  
  15. pieces[piece] += 1;
  16.  
  17. }
  18.  
  19. });
  20.  
  21. if(pieces[ 'X' ] == 3){
  22.  
  23. scores[ 'X' ] += 1;
  24.  
  25. } else if(pieces[ 'O' ] == 3){
  26.  
  27. scores[ 'O' ] += 1;
  28.  
  29. }
  30.  
  31. });
  32.  
  33. // update   current player and opponent score
  34.  
  35. $scope.$apply( function () {
  36.  
  37. $scope.player_score = scores[me.piece];
  38.  
  39. $scope.opponent_score = scores[$scope.opponent_piece];
  40.  
  41. });

Create a main template file

Now, create a template file home.html in the directory www/templates and add the following code:

  1. <ion- view title= "Home" ng-controller= "HomeController as home_ctrl" ng-init= "connect()" >
  2.  
  3. <header class= "bar bar-header bar-stable" >
  4.  
  5. <h1 class= "title" >Ionic Horizon Tic Tac Toe</h1>
  6.  
  7. </header>
  8.  
  9. <ion-content class= "has-header" ng-show= "home_ctrl.ready" >
  10.  
  11. <div id= "join" class= "padding" ng-hide= "home_ctrl.has_joined" >
  12.  
  13. <div class= "list" >
  14.  
  15. <label class= "item item-input" >
  16.  
  17. <input type= "text" ng-model= "home_ctrl.room" placeholder= "Room Name" >
  18.  
  19. </label>
  20.  
  21. <label class= "item item-input" >
  22.  
  23. <input type= "text" ng-model= "home_ctrl.username" placeholder= "User Name" >
  24.  
  25. </label>
  26.  
  27. </div>
  28.  
  29. <button class= "button button-positive button-block" ng-click= "join(home_ctrl.username, home_ctrl.room)" >
  30.  
  31. join  
  32.  
  33. </button>
  34.  
  35. </div>
  36.  
  37. <div id= "game" ng-show= "home_ctrl.has_joined" >
  38.  
  39. <div id= "board" >
  40.  
  41. <div class= "row" >
  42.  
  43. <div class= "col" ng-click= "placePiece(1)" id= "1" ></div>
  44.  
  45. <div class= "col" ng-click= "placePiece(2)" id= "2" ></div>
  46.  
  47. <div class= "col" ng-click= "placePiece(3)" id= "3" ></div>
  48.  
  49. </div>
  50.  
  51. <div class= "row" >
  52.  
  53. <div class= "col" ng-click= "placePiece(4)" id= "4" ></div>
  54.  
  55. <div class= "col" ng-click= "placePiece(5)" id= "5" ></div>
  56.  
  57. <div class= "col" ng-click= "placePiece(6)" id= "6" ></div>
  58.  
  59. </div>
  60.  
  61. <div class= "row" >
  62.  
  63. <div class= "col" ng-click= "placePiece(7)" id= "7" ></div>
  64.  
  65. <div class= "col" ng-click= "placePiece(8)" id= "8" ></div>
  66.  
  67. <div class= "col" ng-click= "placePiece(9)" id= "9" ></div>
  68.  
  69. </div>
  70.  
  71. </div>
  72.  
  73. <div id= "scores" >
  74.  
  75. <div class= "row" >
  76.  
  77. <div class= "col col-50 player" >
  78.  
  79. <div class= "player-name" ng-bind= "player" ></div>
  80.  
  81. <div class= "player-score" ng-bind= "player_score" ></div>
  82.  
  83. </div>
  84.  
  85. <div class= "col col-50 player" >
  86.  
  87. <div class= "player-name" ng-bind= "opponent" ></div>
  88.  
  89. <div class= "player-score" ng-bind= "opponent_score" ></div>
  90.  
  91. </div>
  92.  
  93. </div>
  94.  
  95. </div>
  96.  
  97. </div>
  98.  
  99. </ion-content>
  100.  
  101. </ion-view>

Now, let's analyze the above code. First, a general wrapper is created, which will not be displayed until the user connects to the Horizon server:

  1. <ion-content class= "has-header" ng-show= "home_ctrl.ready" >
  2.  
  3. ...
  4.  
  5. </ion-content>

The form code for joining a game room is as follows:

  1. <div id= "join" class= "padding" ng-hide= "home_ctrl.has_joined" >
  2.  
  3. <div class= "list" >
  4.  
  5. <label class= "item item-input" >
  6.  
  7. <input type= "text" ng-model= "home_ctrl.room" placeholder= "Room Name" >
  8.  
  9. </label>
  10.  
  11. <label class= "item item-input" >
  12.  
  13. <input type= "text" ng-model= "home_ctrl.username" placeholder= "User Name" >
  14.  
  15. </label>
  16.  
  17. </div>
  18.  
  19. <button class= "button button-positive button-block" ng-click= "join(home_ctrl.username, home_ctrl.room)" >
  20.  
  21. join  
  22.  
  23. </button>
  24.  
  25. </div>

The relevant code for the design of the tic-tac-toe chess board is as follows:

  1. <div id= "board" >
  2.  
  3. <div class= "row" >
  4.  
  5. <div class= "col" ng-click= "placePiece(1)" id= "1" ></div>
  6.  
  7. <div class= "col" ng-click= "placePiece(2)" id= "2" ></div>
  8.  
  9. <div class= "col" ng-click= "placePiece(3)" id= "3" ></div>
  10.  
  11. </div>
  12.  
  13. <div class= "row" >
  14.  
  15. <div class= "col" ng-click= "placePiece(4)" id= "4" ></div>
  16.  
  17. <div class= "col" ng-click= "placePiece(5)" id= "5" ></div>
  18.  
  19. <div class= "col" ng-click= "placePiece(6)" id= "6" ></div>
  20.  
  21. </div>
  22.  
  23. <div class= "row" >
  24.  
  25. <div class= "col" ng-click= "placePiece(7)" id= "7" ></div>
  26.  
  27. <div class= "col" ng-click= "placePiece(8)" id= "8" ></div>
  28.  
  29. <div class= "col" ng-click= "placePiece(9)" id= "9" ></div>
  30.  
  31. </div>
  32.  
  33. </div>

The code corresponding to the player score part is as follows:

  1. <div id= "scores" >
  2.  
  3. <div class= "row" >
  4.  
  5. <div class= "col col-50 player" >
  6.  
  7. <div class= "player-name" ng-bind= "player" ></div>
  8.  
  9. <div class= "player-score" ng-bind= "player_score" ></div>
  10.  
  11. </div>
  12.  
  13. <div class= "col col-50 player" >
  14.  
  15. <div class= "player-name" ng-bind= "opponent" ></div>
  16.  
  17. <div class= "player-score" ng-bind= "opponent_score" ></div>
  18.  
  19. </div>
  20.  
  21. </div>
  22.  
  23. </div>

Writing style files

The following is the style definition of the client application:

  1. #board .col {
  2.  
  3. text-align: center;
  4.  
  5. height: 100px;
  6.  
  7. line-height: 100px;
  8.  
  9. font- size : 30px;
  10.  
  11. padding: 0;
  12.  
  13. }
  14.  
  15. #board .col:nth-child(2) {
  16.  
  17. border- right : 1px solid;
  18.  
  19. border- left : 1px solid;
  20.  
  21. }
  22.  
  23. #board .row:nth-child(2) .col {
  24.  
  25. border- top : 1px solid;
  26.  
  27. border-bottom: 1px solid;
  28.  
  29. }
  30.  
  31. .player {
  32.  
  33. font-weight: bold;
  34.  
  35. text-align: center;
  36.  
  37. }
  38.  
  39. .player- name {
  40.  
  41. font- size : 18px;
  42.  
  43. }
  44.  
  45. .player-score {
  46.  
  47. margin- top : 15px;
  48.  
  49. font- size : 30px;
  50.  
  51. }
  52.  
  53. #scores {
  54.  
  55. margin- top : 30px;
  56.  
  57. }

Run the application

Now you can test the application in your browser by executing the following command from the application's root directory:

  1. ionic serve

This will start the server and serve your local project and open a new tab in your default browser.

If you want to test it with friends, you can publish the Horizon server to the internet using Ngrok with the following command:

  1. ngrok http 8181

This command will generate a URL that you can use as the host address when you connect to your Horizon server:

  1. const horizon = Horizon({host: 'xxxx.ngrok.io' });

In addition, change the reference to the horizon.js file in the index.html file:

  1. <script src= "http://xxxx.ngrok.io/horizon/horizon.js" ></script>

To create a mobile version of your app, you need to add a platform (e.g., Android) to your project. This assumes that you have the Android SDK installed on your computer.

  1. ionic platform add android

Next, we generate the .apk file with the following command:

  1. ionic build android

At this point, you can send this .apk file to your friends to play the game together. Of course, you can also play the game by yourself, it's all your business.

summary

In this tutorial, you have developed a very simple application; therefore, there are many aspects that can be improved appropriately to achieve better results. The following are some of the contents for your reference and improvement. Consider these as your skill homework.

Develop a 4×4 or 5×5 version: The 3×3 version you have developed so far will almost always lead to a stalemate, especially if both players playing tic-tac-toe are experts.

Scoring logic: You have to loop a lot to get the player's score. Maybe you can come up with a better solution.

Beautify the game style: The current game style is very plain, in fact, it simulates the tic-tac-toe game suitable for playing on paper.

Add animations: You could try adding a "slide down" animation to the board when a user joins a room, or a "pop up" animation when a player places a card on the board. You can use the animate.css file to implement these types of animations.

Add SNS login support: Adding SNS functionality to such a simple application may be a bit much, but if you want to understand how authentication works in the Horizon framework, this is a good exercise. Using Horizon authentication, you can let users log in to their Facebook, Twitter, or Github accounts.

Add a Play Again feature: You can try adding a "Play Again" button after the game is finished. When this button is pressed, the system will clear the leaderboard and scores so that the player can play again.

Add real-time leaderboard feature: Add a competition leaderboard to show who won the game.

[Translated by 51CTO. Please indicate the original translator and source as 51CTO.com when reprinting on partner sites]

<<:  New features of SpriteKit in iOS 10: Tile Maps (Part 1)

>>:  WOT2016 Wang Qingyou: Listen to the Chief Architect Discussing Large APP Server Architecture

Recommend

How to make 1000 yuan a day. Is there any way to make 1000 yuan a day?

How to make 1000 yuan a day. Is there any way to ...

Understand GUCCI’s Metaverse Marketing in one article!

With the popularity of the Metaverse, related con...

Deep Strike | Why QQ is obsessed with young people

How to log out of QQ? The answer was found some t...

Brand Marketing: Has Platinum Travel Photography over-marketed?

When I was taking the elevator today, the Platinu...

After using gas, should you turn off the gas first or the fire first?

Some people may think You should turn off the gas...

Kuaishou advertising promotion forms and Kuaishou video advertising advantages!

As of early January 2020, the number of monthly a...