Chapter 11 Exercise Set 1

  1. The points on the grid will be represented by objects again. In Searching we used three functions, point, addPoints, and samePoint to work with points. This time, we will use a constructor and two methods. Write the constructor Point, which takes two arguments, the x and y coordinates of the point, and produces an object with x and y properties. Give the prototype of this constructor a method add, which takes another point as argument and returns a new point whose x and y are the sum of the x and y of the two given points. Also add a method isEqualTo, which takes a point and returns a boolean indicating whether the this point refers to the same coordinates as the given point.

    Apart from the two methods, the x and y properties are also part of the interface of this type of objects: Code which uses point objects may freely retrieve and modify x and y.

    function Point(x, y) {
        this.x = x;
        this.y = y;
    }
    Point.prototype.add = function(other) {
        return new Point(this.x + other.x, this.y + other.y);
    };
    Point.prototype.isEqualTo = function(other) {
        return this.x == other.x && this.y == other.y;
    };
    
    alert((new Point(3, 1)).add(new Point(2, 4)));
    

    Make sure your version of add leaves the this point intact and produces a new point object. A method which changes the current point instead would be similar to the += operator, whereas this one is like the + operator.

  2. We will also need to go over all the elements of the grid, to find the bugs we need to move, or to convert the whole thing to a string. To make this easy, we can use a higher-order function that takes an action as its argument. Add the method each to the prototype of Grid, which takes a function of two arguments as its argument. It calls this function for every point on the grid, giving it the point object for that point as its first argument, and the value that is on the grid at that point as second argument.

    Go over the points starting at 0,``0``, one row at a time, so that 1,``0`` is handled before 0,``1``. This will make it easier to write the toString function of the terrarium later. (Hint: Put a for loop for the x coordinate inside a loop for the y coordinate.)

    It is advisable not to muck about in the cells property of the grid object directly, but use valueAt to get at the values. This way, if we decide (for some reason) to use a different method for storing the values, we only have to rewrite valueAt and setValueAt, and the other methods can stay untouched.

    Grid.prototype.each = function(action) {
        for (var y = 0; y < this.height; y++) {
            for (var x = 0; x < this.width; x++) {
                var point = new Point(x, y);
                action(point, this.valueAt(point));
            }
        }
    };
    
  3. Now we can use the each method of the Grid object to build up a string. But to make the result readable, it would be nice to have a newline at the end of every row. The x coordinate of the positions on the grid can be used to determine when the end of a line is reached. Add a method toString, which takes no arguments and returns a string which, when given to print, shows a nice two-dimensional view of the terrarium.

    Terrarium.prototype.toString = function() {
        var characters = [];
        var endOfLine = this.grid.width - 1;
        this.grid.each(function(point, value) {
            characters.push(characterFromElement(value));
            if (point.x == endOfLine)
                characters.push("\n");
        });
        return characters.join("");
    };
    

    And to try it out...

    var terrarium = new Terrarium(thePlan);
    alert(terrarium.toString());
    
  4. In the expression bind(testArray.push, testArray) the name testArray still occurs twice. Can you design a function method, which allows you to bind an object to one of its methods without naming the object twice?

    It is possible to give the name of the method as a string. This way, the method function can look up the correct function value for itself.

    function method(object, name) {
        return function() {
            object[name].apply(object, arguments);
        };
    }
    
    var pushTest = method(testArray, "push");
    
  5. When asking a bug to act, we must pass it an object with information about its current surroundings. This object will use the direction names we saw earlier ("n", "ne", etcetera) as property names. Each property holds a string of one character, as returned by characterFromElement, indicating what the bug can see in that direction.

    Add a method listSurroundings to the Terrarium prototype. It takes one argument, the point at which the bug is currently standing, and returns an object with information about the surroundings of that point. When the point is at the edge of the grid, use "#" for the directions that go outside of the grid, so the bug will not try to move there.

    Hint

    Do not write out all the directions, use the each method on the directions dictionary.

    Terrarium.prototype.listSurroundings = function(center) {
        var result = {};
        var grid = this.grid;
        directions.each(function(name, direction) {
            var place = center.add(direction);
            if (grid.isInside(place))
                result[name] = characterFromElement(grid.valueAt(place));
            else
                result[name] = "#";
        });
        return result;
    };
    

    Note the use of the grid variable to work around the this problem.

  6. Create a bug type called DrunkBug which tries to move in a random direction every turn, never mind whether there is a wall there. Remember the Math.random trick from Searching.

    To pick a random direction, we will need an array of direction names. We could of course just type ["n", "ne", ...], but that duplicates information, and duplicated information makes me nervous. We could also use the each method in directions to build the array, which is better already.

    But there is clearly a generality to be discovered here. Getting a list of the property names in a dictionary sounds like a useful tool to have, so we add it to the Dictionary prototype.

    Dictionary.prototype.names = function() {
        var names = [];
        this.each(function(name, value) {names.push(name);});
        return names;
    };
    
    alert(directions.names());
    

    A real neurotic programmer would immediately restore symmetry by also adding a values method, which returns a list of the values stored in the dictionary. But I guess that can wait until we need it.

    Here is a way to take a random element from an array:

    function randomElement(array) {
        if (array.length == 0)
            throw new Error("The array is empty.");
        return array[Math.floor(Math.random() * array.length)];
    }
    
    alert(randomElement(["heads", "tails"]));
    

    And the bug itself:

    function DrunkBug() {};
    DrunkBug.prototype.act = function(surroundings) {
        return {type: "move", direction: randomElement(directions.names())};
    };
    DrunkBug.prototype.character = "~";
    
    creatureTypes.register(DrunkBug);
    
  7. Create a LichenEater creature. It starts with an energy of 10, and behaves in the following way:

    • When it has an energy of 30 or more, and there is room near it, it reproduces.
    • Otherwise, if there are lichen nearby, it eats a random one.
    • Otherwise, if there is space to move, it moves into a random nearby empty square.
    • Otherwise, it waits.

    Use findDirections and randomElement to check the surroundings and to pick directions. Give the lichen-eater "c" as its character (pac-man).

    function LichenEater() {
        this.energy = 10;
    }
    LichenEater.prototype.act = function(surroundings) {
        var emptySpace = findDirections(surroundings, " ");
        var lichen = findDirections(surroundings, "*");
    
        if (this.energy >= 30 && emptySpace.length > 0)
            return {type: "reproduce", direction: randomElement(emptySpace)};
        else if (lichen.length > 0)
            return {type: "eat", direction: randomElement(lichen)};
        else if (emptySpace.length > 0)
            return {type: "move", direction: randomElement(emptySpace)};
        else
            return {type: "wait"};
    };
    LichenEater.prototype.character = "c";
    
    creatureTypes.register(LichenEater);
    
  8. Find a way to modify the LichenEater to be more likely to survive. Do not cheat ― this.energy += 100 is cheating. If you rewrite the constructor, do not forget to re-register it in the creatureTypes dictionary, or the terrarium will continue to use the old constructor.

    One approach would be to reduce the randomness of its movement. By always picking a random direction, it will often move back and forth without getting anywhere. By remembering the last direction it went, and preferring that direction, the eater will waste less time, and find food faster.

    function CleverLichenEater() {
        this.energy = 10;
        this.direction = "ne";
    }
    CleverLichenEater.prototype.act = function(surroundings) {
        var emptySpace = findDirections(surroundings, " ");
        var lichen = findDirections(surroundings, "*");
    
        if (this.energy >= 30 && emptySpace.length > 0) {
            return {type: "reproduce", direction: randomElement(emptySpace)};
        }
        else if (lichen.length > 1) {
            return {type: "eat", direction: randomElement(lichen)};
        }
        else if (emptySpace.length > 0) {
            if (surroundings[this.direction] != " ")
                this.direction = randomElement(emptySpace);
            return {type: "move", direction: this.direction};
        }
        else {
            return {type: "wait"};
        }
    };
    CleverLichenEater.prototype.character = "c";
    
    creatureTypes.register(CleverLichenEater);
    

    Try it out using the previous terrarium-plan.

  9. A one-link food chain is still a bit rudimentary. Can you write a new creature, LichenEaterEater (character "@"), which survives by eating lichen-eaters? Try to find a way to make it fit in the ecosystem without dying out too quickly. Modify the lichenPlan array to include a few of these, and try them out.

    You are on your own here. I failed to find a really good way to prevent these creatures from either going extinct right away or gobbling up all lichen-eaters and then going extinct. The trick of only eating when it spots two pieces of food doesn’t work very well for them, because their food moves around so much it is rare to find two in one place. What does seem to help is making the eater-eater really fat (high energy), so that it can survive times when lichen-eaters are scarce, and only reproduces slowly, which prevents it from exterminating its food source too quickly.

    The lichen and eaters go through a periodic movement ― sometimes lichen are abundant, which causes a lot of eaters to be born, which causes the lichen to become scarce, which causes the eaters to starve, which causes the lichen to become abundant, and so on. You could try to make the lichen-eater-eaters ‘hibernate’ (use the "wait" action for a while), when they fail to find food for a few turns. If you choose the right amount of turns for this hibernation, or have them wake up automatically when they smell lots of food, this could be a good strategy.