Building Sokoban with Polymer

Emre Guneyler
Emre Guneyler
Share

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.

Frequently Asked Questions (FAQs) about Building Sokoban with Polymer

What is Polymer and why is it used in building Sokoban?

Polymer is a JavaScript library that helps developers create custom, reusable HTML elements, and use them to build performant, maintainable apps. It’s used in building Sokoban because it allows for the creation of custom elements that can be used throughout the game, making the code more organized and easier to manage. Polymer also supports two-way data binding, which is useful in game development as it allows for real-time updates in the game state.

How can I install Polymer?

To install Polymer, you need to have Node.js and npm installed on your computer. Once you have these, you can install Polymer CLI, a command-line interface for Polymer projects, by running the command npm install -g polymer-cli in your terminal or command prompt.

How can I create custom elements in Polymer?

Creating custom elements in Polymer involves defining a new class that extends HTMLElement and then registering it with a tag name. You can then use this tag name as a regular HTML element. The process involves using the customElements.define() method and the class keyword to create a new class for the custom element.

How does the Sokoban game logic work?

The Sokoban game logic involves moving the player around a grid, pushing boxes onto target locations. The player can only push boxes, not pull them, and can only push one box at a time. The game is won when all boxes are on target locations.

How can I handle player movements in Sokoban?

Player movements in Sokoban can be handled by listening for keydown events and updating the player’s position based on the key pressed. You can use an object to map key codes to directions and then update the player’s position accordingly.

How can I check for game win in Sokoban?

You can check for a game win in Sokoban by checking if all boxes are on target locations. This can be done by iterating over all boxes and checking if their current position matches any of the target locations.

How can I display the game state in Sokoban?

The game state in Sokoban can be displayed using Polymer’s two-way data binding. You can bind the game state to your custom elements and they will automatically update whenever the game state changes.

How can I handle box movements in Sokoban?

Box movements in Sokoban can be handled similarly to player movements. When the player moves into a box, you can check the direction of movement and move the box in the same direction if the next cell in that direction is free.

How can I create the game grid in Sokoban?

The game grid in Sokoban can be created using a two-dimensional array. Each cell in the array represents a cell in the game grid and can hold a value representing a wall, a box, a target location, or an empty space.

How can I style my Sokoban game?

You can style your Sokoban game using CSS. Polymer supports encapsulated styling, which means you can define styles for your custom elements that won’t affect other elements on the page. You can also use CSS variables to create a consistent look and feel across your game.