Chapter 7 Exercise Set 1

  1. Write a function called registerEventHandler to wrap the incompatibilities of these two models. It takes three arguments: first a DOM node that the handler should be attached to, then the name of the event type, such as "click" or "keypress", and finally the handler function.

    To determine which method should be called, look for the methods themselves ― if the DOM node has a method called attachEvent, you may assume that this is the correct method. Note that this is much preferable to directly checking whether the browser is Internet Explorer. If a new browser arrives which uses Internet Explorer’s model, or Internet Explorer suddenly switches to the standard model, the code will still work. Both are rather unlikely, of course, but doing something in a smart way never hurts.

    function registerEventHandler(node, event, handler) {
        if (typeof node.addEventListener == "function")
            node.addEventListener(event, handler, false);
        else
            node.attachEvent("on" + event, handler);
    }
    
    registerEventHandler($("button"), "click",
                         function(){print("Click (2)");});
    

    Don’t fret about the long, clumsy name. Later on, we will have to add an extra wrapper to wrap this wrapper, and it will have a shorter name.

    It is also possible to do this check only once, and define registerEventHandler to hold a different function depending on the browser. This is more efficient, but a little strange.

    if (typeof document.addEventListener == "function")
        var registerEventHandler = function(node, event, handler) {
            node.addEventListener(event, handler, false);
        };
    else
        var registerEventHandler = function(node, event, handler) {
            node.attachEvent("on" + event, handler);
        };
    
  2. Add methods moveContent and clearContent to the Square prototype. The first one takes another Square object as an argument, and moves the content of the this square into the argument by updating the content properties and moving the image node associated with this content. This will be used to move boulders and players around the grid. It may assume the square is not currently empty. clearContent removes the content from the square without moving it anywhere. Note that the content property for empty squares contains null.

    The removeElement function we defined in The Document-Object Model is available in this chapter too, for your node-removing convenience. You may assume that the images are the only child nodes of the table cells, and can thus be reached through, for example, this.tableCell.lastChild.

    Square.moveContent = function(target) {
        target.content = this.content;
        this.content = null;
        target.tableCell.appendChild(this.tableCell.lastChild);
    };
    Square.clearContent = function() {
        this.content = null;
        removeElement(this.tableCell.lastChild);
    };
    
  3. All that is left to do now is filling in the key event handler. Replace the keyDown method of the prototype with one that detects presses of the arrow keys and, when it finds them, moves the player in the correct direction. The following Dictionary will probably come in handy:

    var arrowKeyCodes = new Dictionary({
        37: new Point(-1, 0), // left
        38: new Point(0, -1), // up
        39: new Point(1, 0),  // right
        40: new Point(0, 1)   // down
    });
    

    After an arrow key has been handled, check this.field.won() to find out if that was the winning move. If the player won, use alert to show a message, and go to the next level. If there is no next level (check sokobanLevels.length), restart the game instead.

    It is probably wise to stop the events for key presses after handling them, otherwise pressing arrow-up and arrow-down will scroll your window, which is rather annoying.

    SokobanGame.keyDown = function(event) {
        if (arrowKeyCodes.contains(event.keyCode)) {
            event.stop();
            this.field.move(arrowKeyCodes.lookup(event.keyCode));
            if (this.field.won()) {
                if (this.level < sokobanLevels.length - 1) {
                    alert("Excellent! Going to the next level.");
                    this.level++;
                    this.reset();
                }
                else {
                    alert("You win! Game over.");
                    this.newGame();
                }
            }
        }
    };
    

    It has to be noted that capturing keys like this ― adding a handler to the document and stopping the events that you are looking for ― is not very nice when there are other elements in the document. For example, try moving the cursor around in the text field at the top of the document. ― It won’t work, you’ll only move the little man in the Sokoban game. If a game like this were to be used in a real site, it is probably best to put it in a frame or window of its own, so that it only grabs events aimed at its own window.

  4. When brought to the exit, the boulders vanish rather abrubtly. By modifying the Square.clearContent method, try to show a “falling” animation for boulders that are about to be removed. Make them grow smaller for a moment before, and then disappear. You can use style.width = "50%", and similarly for style.height, to make an image appear, for example, half as big as it usually is.

    We can use setInterval to handle the timing of the animation. Note that the method makes sure to clear the interval after it is done. If you don’t do that, it will continue wasting your computer’s time until the page is closed.

    Square.clearContent = function() {
        self.content = null;
        var image = this.tableCell.lastChild;
        var size = 100;
    
        var animate = setInterval(function() {
            size -= 10;
            image.style.width = size + "%";
            image.style.height = size + "%";
    
            if (size < 60) {
                clearInterval(animate);
                removeElement(image);
            }
        }, 70);
    };
    

    Now, if you have a few hours to waste, try finishing all levels.

  5. But this field doesn’t do very much yet. Add a method called move. It takes a Point object specifying the move as argument (for example -1,0 to move left), and takes care of moving the game elements in the correct way.

    The correct way is this: The playerPos property can be used to determine where the player is trying to move. If there is a boulder here, look at the square behind this boulder. When there is an exit there, remove the boulder and update the score. When there is empty space there, move the boulder into it. Next, try to move the player. If the square he is trying to move into is not empty, ignore the move.

    SokobanField.move = function(direction) {
        var playerSquare = this.getSquare(this.playerPos);
        var targetPos = this.playerPos.add(direction);
        var targetSquare = this.getSquare(targetPos);
    
        // Possibly pushing a boulder
        if (targetSquare.hasBoulder()) {
            var pushTarget = this.getSquare(targetPos.add(direction));
            if (pushTarget.isEmpty()) {
                targetSquare.moveContent(pushTarget);
            }
            else if (pushTarget.isExit()) {
                targetSquare.moveContent(pushTarget);
                pushTarget.clearContent();
                this.bouldersToGo--;
                this.updateScore();
            }
        }
        // Moving the player
        if (targetSquare.isEmpty()) {
            playerSquare.moveContent(targetSquare);
            this.playerPos = targetPos;
        }
    };
    

    By taking care of boulders first, the move code can work the same way when the player is moving normally and when he is pushing a boulder. Note how the square behind the boulder is found by adding the direction to the playerPos twice. Test it by moving left two squares:

    testField.move(new Point(-1, 0));
    testField.move(new Point(-1, 0));
    

    If that worked, we moved a boulder into a place from which we can’t get it out anymore, so we’d better throw this field away.

    testField.remove();