Dart and PHP: A Legacy Animal Guess Game

Taylor Ren
Tweet

Back when I was learning programming on Apple II using BASIC, there was an Animal Guess Game. This game was a very primitive AI game: the computer tries to ask a few YES/NO questions and receives the answer from the user. Based on the answer, it may ask more Y/N questions until it tries to guess the animal.

In this tutorial, we will learn how to re-vitalize this program using PHP as backend and Dart as frontend. Of course, a database will be used to store all the questions and guesses of animals.

The complete code has been uploaded to Github. You can clone it from here.

Database Setup

The database structure for this program is simple. We only need one table:

CREATE TABLE `animal` (
  `id` int(11) NOT NULL,
  `question` varchar(140) DEFAULT NULL,
  `y_branch` int(11) DEFAULT NULL,
  `n_branch` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
)

id is used to identify each question/guess; question is the question to be asked or the guess to be prompted; y_branch and n_branch identifies the question id when the user answers Yes or No to the question. In particular, if both these two fields are "-1", it means there are no more questions to be asked (and the program has reached the guess stage).

The SQL structure and the initial data (one question and two animals) can be found in the animal.sql file.

Backend

As the backend is relatively simple, I will use plain PHP (with PDO). The files are located under the server directory in the repository. The server basically has two functions:

  1. Get a question or prompts a guess by ID;
  2. Split a node with new questions and new guesses with user's input;

We will take a look at the get question function:

    <?php

    require_once 'pdo.php';

    $id=filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

    if(!$id)
        $id=1;

    $res=$cn->prepare('select * from animal where id = :id');
    $res->bindParam(':id', $id);
    $res->execute();

    $r=$res->fetch();

    $ret=array();
    $ret['q']=$r[1];
    $ret['y']=$r[2];
    $ret['n']=$r[3];

    setExtraHeader();

    echo json_encode($ret);
    ?>

In this get.php file, we included a pdo.php file to setup the database connection. Then we process the input and do the query. Finally, we output the result to the frontend (the Dart app in this case).

A few things to notice here:

  1. All results returned to Dart app should be in JSON format. Thus, we use the json_encode function to encode the array.
  2. Before we actually return the result, we set some extra HTTP headers to enable CORS. Although all our files are "physically" on the same machine, the Dart app and the back end are actually running on two different domains. Without the extra headers, the call from frontend to backend will fail. The setExtraHeader function is also defined in pdo.php.

Frontend

Front end web programming has been very much facilitated (or complicated?) through HTML5, JavaScript and other 3rd-party libraries. It just needs to be a lot more structured.

In this tutorial, we will use Google's Dart as the tool for front end development.

Installation

To get the Dart IDE, please visit https://www.dartlang.org and download the package for your platform. The installation is straightforward. Alternatively, download Webstorm which includes native Dart support and is much more stable and performant than the Eclipse based Dart Editor.

Dart has just released its stable version and taken off its long-wearing "BETA" hat, but it is evolving rapidly. At the time of writing, I am using Dart Editor and SDK version 1.0.0_r30188 (STABLE).

To fully utilize the interactivity that Dart offers, we will use the new Polymer library.

Note: Polymer replaces the web_ui library in older Dart releases. Like Dart, Polymer is also rapidly evolving. The one I used in this program is version 0.9.0+1. Some of the syntax and features may be different in future versions.

Polymer offers some useful features when developing the front end like custom HTML elements, bi-directional data-binding, conditional templates, asynchronous remote function calls, etc. All these features will be used in this program.

Create a Polymer application

Start Dart IDE, and select "File|New Application". Be sure to choose "Web application (using the polymer library)" as the application type.

The wizard will create the app directory and set up all necessary dependencies. As we chose to "Generate sample content", it will also create a few sample files. We can delete all these sample files except pubspec.yaml.

Right click on the pubspec.yaml file and choose Pub Get from the menu. This will help install all the necessary libraries for a Dart/Polymer app.

A typical Polymer app contains at least 3 files:

  1. An HTML file as the app entry point. In this case: web/animalguess.html. In this file, normally we will setup the basic structure for an HTML file, and MUST instantiate a custom HTML element.
  2. An HTML file that defines a custom HTML element, the layout, the script for that element, etc. In this case: web/animalguessclass.html.
  3. A DART file that implements the functionality for that custom HTML element.

Let's discuss the key points of each file.

animalguess.html

animalguess.html file defines the overall layout of the app. It is an HTML5 compliant file with all the regular HEAD, TITLE, LINK, SCRIPT, META elements, along with a custom HTML element tag.

    <!DOCTYPE html>

    <html>
    <head>
    <meta charset="utf-8">
    <title>Welcome to Animal Guess Game!</title>
    <link rel="stylesheet" href="css/bootstrap.css">
    <link rel="stylesheet" href="css/animal.css">

    <!-- import the underlying class -->
    <link rel="import" href="animalguessclass.html">
    <script type="application/dart">import 'package:polymer/init.dart';</script>
    <script src="packages/browser/dart.js"></script>
    </head>
    <body>
          <div class="container">
            <h1>Welcome to the legacy Animal Guess Game!</h1>
            <p><em>Revitalized with PHP and Dart!</em></p>
          </div>
          <hr>
        <animal-guess></animal-guess>
    </body>
    </html>

For the most part of the <head></head> section, we really don't need to change anything. For this app, I only changed the two CSS links to link to Bootstrap CSS and my further customized CSS.

In the BODY section, we have included the custom HTML element <animal-guess>. This element is defined in animalguessclass.html and imported via the <link rel="import" href="animalguessclass.html"> statement.

animalguessclass.html and the custom element

This file defines the layout, template, behavior of the custom HTML element. However, the actual code to implement the behavior is usually defined in a separate DART file (animalguessclass.dart).

    <polymer-element name="animal-guess"> 
        <template>

            <div class="container">    
              <template if="{{!gameinprogress}}">
                  <h3>Let's get started!</h3>
                  <button on-click="{{newGame}}">Click me</button>
              </template>

          ...
              <template if="{{gameinprogress}}">
                  <div class="row">
                    <div class="col-md-6">{{qid}}. {{question}}</div>
                        <template if="{{!reachedend}}">
                            <div class="col-md-6">
                                  <a href="#" on-click="{{YBranch}}">Yes</a>&nbsp;&nbsp;<a href="#"
                            on-click="{{NBranch}}">No</a>
                            </div>
                        </template>
                      </div>
              </template>
        ...          
        </template>
        <script type="application/dart" src="animalguessclass.dart"></script>
    </polymer-element>

The above excerpt shows the fundamental structure of an HTML file for a Polymer element.

<polymer-element name="animal-guess"></polymer-element> must be presented to define an element. Please note the name attribute. It has the same value we use in animalguess.html ("animal-guess").

There are conditional template instantiations. For example:

    <template if="{{!gameinprogress}}">
          <h3>Let's get started!</h3>
        <button on-click="{{newGame}}">Click me</button>
      </template>

The HTML code between <template></template> won't be rendered unless gameinprocess is false. gameinprogress is a variable that will be elaborated later.

Also, note that we have hooked a button element's click event to an event handler ("newgame"). We will also discuss this later.

Generally speaking, this HTML file isn't that different from a traditional HTML file, or an HTML template. We can use all kinds of HTML elements in this file.

Note: Radio buttons can be used. But there are some issues related to the binding of the value. So in this implementation, we only use text boxes for input. There may be issues related to data binding for other types of form controls, but we are not covering this topic here.

Also, in this file, we declared that we will use animalguessclass.dart as the script for this element.

The complete code for animalguessclass.html can be found in the web directory.

animalguessclass.dart

This file is the driver for this app. It has all the logic that drives the behavior of the program. Let's take a look at some key sections.

import 'package:polymer/polymer.dart';
import 'dart:html';
import 'dart:convert';

@CustomTag('animal-guess')
class AnimalGuess extends PolymerElement {
  @published bool gameinprogress=false;
  @published String question='';
  @published String myguess='';
  @published int qid=1;
  int yBranch;
  int nBranch;
  ...

  AnimalGuess.created() : super.created() {
    // The below 2 lines make sure the Bootstrap CSS will be applied
    var root = getShadowRoot("animal-guess");
    root.applyAuthorStyles = true;
  }

  void newGame() {
    gameinprogress=true;
    win=false;
    lost=false;
    reachedend=false;
    qid=1;
    getQuestionById(qid);
  }

  void getQuestionById(qid)
  {
    var path='http://animal/get.php?id=$qid';
    var req=new HttpRequest();
    req..open('GET', path)
      ..onLoadEnd.listen((e)=>requestComplete(req))
      ..send('');
  }

  void requestComplete(HttpRequest req)
  {
    if (req.status==200)
    {
      Map res=JSON.decode(req.responseText);
      myguess=res['q'];
      yBranch=res['y'];
      nBranch=res['n'];

      if (yBranch==-1 && nBranch==-1) // No more branches and we have reached the "guess"
      {
        question='Is it a/an $myguess?';
      }
      else
      {
        question=myguess;
      }
    }
  }
}

The first 3 import statements import the necessary libraries used in this script. When using Polymer and DOM, the first two are required, and when decoding JSON we also need the third. To see other packages and libraries, see the API reference and the package repository.

@CustomTag('animal-guess') defines the custom tag we will be using. It has the same name as it appears in animalguess.html and animalguessclass.html.

In the class definition, we see a few variable declarations. Polymer uses @published keyword to declare a "public" variable (like gameinprogress flag that indicates if the game is started and is used to decide which template to show) and it will be accessible in the script as well as in the related html file (animalguessclass.html). By doing so, we have created "bi-directional" data binding.

The rest are the function declarations. Most of the functions will be "event-handlers" to the "on-click" events in the aforementioned animalguess.html. Other types of event handlers are also available.

A few things of note:

  • In the class constructor, we do a trick to make sure the Bootstrap CSS can be applied to our custom HTML tag ("animal-guess"). The issue here is elaborated in this article from Stackoverflow. Basically, Bootstrap "doesn't know about ShadowDOM and attempts to fetch nodes from the DOM using global selectors." But in Polymer, we are almost mandated to use a custom element and Shadow DOM is in existence. So the "turnaround" is just to make sure the ShadowDOM we created will work with Bootstrap and have the CSS styles we wanted.
  • The callback function (requestComplete) is hooked up to the HttpRequest object. The syntax used is new in Polymer and is called "chained" method calling. It is different from a single dot notation and uses two dots. It is equivalent to the below 3 statements:
req.open(...);
req.onLoadEnd(...)...;
req.send(...);
  • In the requestComplete function, we first test the HTTP status code (200 means everything is OK) and then use a Map type variable to store the decoded JSON object. That variable will have an exact "key-value" pair as the JSON result that is returned from our remote server. In our case, the back end "remote" server is on the same machine (running on 80 port) and the app, when launched in Dart, will be on 3030 port. So in this sense, they are in two different domains and CORS headers must be presented in the returned HTTP response.

Below is a screen shot when the computer exhausts its questions but makes a wrong guess. Then it prompts for a new question to distinguish its guess and the user's animal:

At this point, the app is already functional: back end to provide the data and the front end to provide the app logic and presentation. At least one improvement can be done: using radio button form controls to receive answers for the new question and restrain the user's input. I'll leave that up to you.

Deploy to be a standalone app

The current program can only be run in Dart's own browser (a highly customized Chrome-based browser that supports Dart interpreter – you automatically download it when you download the Dart SDK). To make the app standalone, we can compile the Dart app into JavaScript.

To do so, click on the "build.dart" file in project root, and select "Tools | Pub Build. After some processing, a new "build" directory will appear in the project root directory which contains all the files required to run it as a standalone app. We can simply copy all those files into a site and it will be up and running.

Conclusion

In this tutorial, we re-vitalized a legacy Guess Animal AI Game using modern technologies: a database, Dart and PHP. The purpose of the tutorial was to demonstrate a seamless integration of all parts and make a useful rich web application in a very structured way.

If you have some feedback or questions, please leave them in the comments below and I'll address them promptly. If you've found this article interesting and would like to see it expanded on, please share it with your friends and colleagues so we can gauge the interest and plan accordingly.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • sethladd

    Thanks for the write up! Can I suggest trying HttpRequest.getString() instead of using the old .send and .open API?

    • Taylor Ren

      Hi Seth,

      Thanks for point out this. The reason I am using the .send . open is that in my 1st draft I used getString but something very funny happens and can’t really get data back. It might due to the unstable version I was using then.

      Later when Dart is in 1.0, just forgot this point. Will test to see the outcome later and let you know.