JavaScript
Article

Building Sokoban with Polymer

By Emre Guneyler

When I first heard about Polymer, I thought about my old Silverlight days. Silverlight used XHTML for markup and C# for the code. Polymer is similar, but Polymer uses HTML and Javascript. See this excellent article for an introduction to Polymer. In this tutorial we will build the classic Sokoban game, leveraging the use of Web Components and an excellent Yeoman generator, generator-polymer, and publish it using Bower.

Setup Polymer

Setting up a Polymer project is as simple as the following two commands:

$ npm install generator-polymer -g
$ yo polymer

It will ask you to include some standard components. Since we don’t need any, you can say no to all.

This is the generated folder structure. All custom elements are in app/elements folder.

.
|-- Gruntfile.js
|-- app
|   |-- elements
|   |   |-- elements.html
|   |   |-- soko-ban
|   |   |   |-- soko-ban.html
|   |   |   `-- soko-ban.scss
|   |-- index.html
|   |-- scripts
|   |   |-- app.js
|-- bower.json
`-- package.json

To begin development run grunt serve. It will serve index.html and watch the files for live-reloading as they change. This is the index.html, I’ve only included the essential parts to use Polymer.

<html>
  <head>
    <script src="bower_components/platform/platform.js"></script>
    <!-- build:vulcanized elements/elements.vulcanized.html -->
    <link rel="import" href="elements/elements.html">
    <!-- endbuild -->
  </head>

  <body unresolved>
    <div class="game-container">
      <!-- insert your elements here -->
      <soko-ban></soko-ban>
    </div>

    <script src="scripts/app.js"></script>
  </body>
</html>

We include platform.js to enable Polymer, and import elements.html that further imports all our elements. Notice it’s wrapped in a build:vulcanized build block that will concatenate all our imported elements in a single file. Finally in the body we add our custom Polymer elements. I’ve included the final element that we will be building, sokoban-ban, you can replace it with the other sub elements to test them out as you build.

Custom Element: sprite-el

The first custom element we will build is a sprite element, this will serve as the base for all the sprites, such as boxes and our player. To add a custom element run a single command.

$ yo polymer:el sprite-el

This will create the elements/sprite-el sub-folder and add two files, sprite-el.html and sprite-el.scss. It will also inject sprite-el.html in elements.html, basically doing the boilerplate for you.

See sprite-el.html injected into elements.html by Yeoman.

File: elements/elements.html

<link rel="import" href="sprite-el/sprite-el.html">

Element Declaration

Let’s define our custom element sprite-el.

<link rel="import" href="../../bower_components/polymer/polymer.html">
<polymer-element name="sprite-el">
  <template>
    <link rel="stylesheet" href="sprite-el.css">
    <div class="sprite" style="top: {{posY}}px; left: {{posX}}px; height: {{frame.height}}px; width: {{frame.width}}px; background: url({{spriteUrl}}) {{frame.x}}px {{frame.y}}px">
    </div>
  </template>
  <script>
    (function () {
      'use strict';

      Polymer({
       publish: {
         spriteUrl: 'images/sprites.png',
         frame: {
           x: 0,
           y: 0
         },
         position: {
           x: 0,
           y: 0
         },

         computed: {
           posX: 'position.x * 64',
           posY: 'position.y * 64'
         }
       }
     });

    })();
  </script>
</polymer-element>

First we include polymer.html, and open a polymer-element tag, with sprite-el name attribute, which is required and must include a -. Next, we have two sub tags, template and script. template contains markup for our custom element. Within script we call the Polymer function to fire up the custom element. For more info see the documentation.

Element Template

In the template, we include the style sprite-el.css that is compiled by Grunt from sprite-el.scss.

Next, we have a div with a sprite class, and style attribute. style attribute defines top, left, height, width, and background, styling to decide the position, and bounds of the sprite and it’s image. We include these styles inline because we have to use data-binding for these style attributes.

Data Binding, Published, and Computed Properties

Properties on the element, can be bound directly into the view, with Polymer expressions, like {{posY}}, {{frame.height}}, {{spriteUrl}}.

posX and posY are defined under computed property, which indicates these are computed properties. They are dynamic properties, that are computed based on other property values. In our case they depend on position.x and position.y so whenever position property changes they are recalculated and updated in the view too.

spriteUrl and frame are published properties. That means you are making that property part of the element’s “public API”. So, the users of the element can change them. Published properties are also data-bound and are accessible via {{}}.

Custom Element: box-el

The next custom element is a box element, this will be composed of our sprite-el, and will represent the boxes, walls, and the ground. Let’s bother Yeoman once more.

$ yo polymer:el box-el

Game Art and Sprite Frames

All the game art is taken from 1001.com and are licensed CC-BY-SA 4.0. You can find all the sprites and full source code on GitHub.

We have five sprite frames – B for boxes, BD for dark boxes, T for target, W for walls, and G for ground. It’s actually better to define moving boxes and background sprites within separate layers, but for simplicity we’re including all of them in one element. Each frame defines the frame position in the sprite-sheet as well as its height and width.

Let’s define our custom element box-el:

<polymer-element name="box-el">
  <template>
    <link rel="stylesheet" href="box-el.css">
    <sprite-el frame="{{frame}}" position="{{model.position}}" style="height: {{frame.height}}px; width: {{frame.width}}px;"></sprite-el>
  </template>
  <script>
    (function () {
      'use strict';

      Polymer({
       publish: {
         model: {
           position: {
             x: 0,
             y: 0
           },
           type: 'W'
         }
       },

       computed: {
         frame: 'boxCoords[model.type]'
       },
       
       ready: function() {
         this.boxCoords = {
           "B": { x:"-192", y:"0", width:"64", height:"64" },
           "BD": { x:"-128", y:"-256", width:"64", height:"64" },
           "T": { x:"-64", y:"-384", width:"32", height:"32" },
           "W": { x:"0", y:"-320", width:"64", height:"64" },
           "G": { x:"-64", y:"-256", width:"64", height:"64" }
         };
       }
      });

    })();
  </script>
</polymer-element>

Inheritance and Composition

The box and the player elements will be using the base sprite element. There are two ways to do this, using inheritance or composition. We will not extend sprite-el, but rather use composition. For more information about inheritance see this blog post and this reference.

We include sprite-el in our template, and assign it’s attributes. Remember the published properties frame and position? Here we assign them via attributes.

Lifecycle Methods

One extra property box-el has other than published and computed properties is ready lifecycle method. ready lifecycle method is called when the element is fully prepared, we can assign extra properties in this callback, in our case it’s boxCoords which is used by frame computed property.

Custom Element: sokoban-el

Our final custom element is the Sokoban game itself. This will be composed of our player-el, and box, wall, and ground elements.

Game Model, Game Controller, and Input Manager

All the game logic is inside GameController type. It generates the game map, and directly manipulates the game model. The game model is data bounded to our view, that is the polymer element. So all the changes to the model made by GameController is automatically updated in the view. I won’t get into detail about the game logic in this article, you can check out the full source code for more details.

Handling user input can be done using declarative event mapping. But, yet there are some caveats. See this question on Stack Overflow. So I used a custom type to handle input, KeyboardInputManager.

Let’s define our custom element soko-ban:

<polymer-element name="soko-ban">
  <template>
    <link rel="stylesheet" href="soko-ban.css">
    <template repeat="{{box in boxes}}">
      <box-el model="{{box}}"></box-el>
    </template>
    <player-el model="{{player}}" id="character"></player-el>
  </template>
  <script>
    (function () {
      'use strict';
     
      Polymer({
       ready: function() {

         var controller = new GameController();
         var model = controller.getModel();

         /** Sample Model **/
         /**
         this.player = {
           position: {
             x: 0,
             y: 0
           }
         };

         this.boxes = [
           {
             type: 'W',
             position: {
               x: 10,
               y: 10
             }
           },
           {
             type: 'WD',
             position: {
               x: 10,
               y: 100
             }
           }
         ];
         */

         this.player = model.player;
         this.boxes = model.boxes;
         
         var inputManager = new KeyboardInputManager();
         var char = this.$.character;
         
         inputManager.on('move', function(val) {
           switch (val) {
             case KeyboardInputManager.Direction.UP:
               controller.move(GameController.Direction.UP);
               break;
             case KeyboardInputManager.Direction.RIGHT:
               controller.move(GameController.Direction.RIGHT);
               break;
             case KeyboardInputManager.Direction.DOWN:
               controller.move(GameController.Direction.DOWN);
               break;
             case KeyboardInputManager.Direction.LEFT:
               controller.move(GameController.Direction.LEFT);
               break;
           }

           if (controller.isGameOver()) {
             this.fire('finished', { target: model.target });
           }
         }.bind(this));
       }
     });
     
    })();
  </script>
</polymer-element>

Note the two properties on our Polymer element player and boxes, we set them to our model. You can manually set them to hard coded values, as you can see in the commented code, for testing purposes.

Iterative Templates

The boxes property is an array of values. We can generate a single template instance for each item in the array. Note the usage of the template tag and repeat attribute to iterate over the array of boxes. See the documentation for more information.

Firing Custom Events

You can also fire custom events within your Polymer element using the fire method. In our case, we fire a finished event when the game is over. You can listen for events as shown below.

document.querySelector('soko-ban')
        .addEventListener('finished', function(e) {
          alert('Congratz you have pushed all ' +
          e.detail.target + ' boxes!');
});

Publish It

We used generator-polymer for building our application. There is also another generator, generator-element, and a Polymer boilerplate template for building and publishing custom elements. Once you have built your custom element with the generator, you can publish it using Bower. For more information on publishing, see these excellent articles, here and here.

Don’t forget to add the web-component tag to your bower.json. Once you have published it to Bower, your element should be available on the Bower registry. Also make sure to submit it to customelements.io.

Find Out More and Live Demo

In this tutorial we’ve seen Polymer in action by building Sokoban. Generally, you don’t have to build your own custom element, you can use existing ones, composing them to build more engaging elements. Visit the web components gallery at customelements.io.

You can do more with Polymer which we haven’t covered such as styling elements, observing properties, etc. For more information visit the API developer guide. You can find the full source code for this project on GitHub, and see a live demo on my site.

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.