JavaScript 38 – progetto: vita elettronica – 1

Continuo da qui, copio qui.

[…] the question of whether Machines Can Think […] is about as relevant as the question of whether Submarines Can Swim.
Edsger Dijkstra

In “project” chapters, I’ll stop pummeling you with new theory for a brief moment and instead work through a program with you. Theory is indispensable when learning to program, but it should be accompanied by reading and understanding nontrivial programs.

Our project is to build a virtual ecosystem, a little world populated with critters that move around and struggle for survival.

Definizione
To make this task manageable, we will radically simplify the concept of a world. Namely, a world will be a two-dimensional grid where each entity takes up one full square of the grid. On every turn, the critters all get a chance to take some action.

Thus, we chop both time and space into units with a fixed size: squares for space and turns for time. Of course, this is a somewhat crude and inaccurate approximation. But our simulation is intended to be amusing, not accurate, so we can freely cut such corners.

We can define a world with a plan, an array of strings that lays out the world’s grid using one character per square.

var plan = ["############################",
            "#      #    #      o      ##",
            "#                          #",
            "#          #####           #",
            "##         #   #    ##     #",
            "###           ##     #     #",
            "#           ###      #     #",
            "#   ####                   #",
            "#   ##       o             #",
            "# o  #         o       ### #",
            "#    #                     #",
            "############################"];

The “#” characters in this plan represent walls and rocks, and the “o” characters represent critters. The spaces, as you might have guessed, are empty space.

A plan array can be used to create a world object. Such an object keeps track of the size and content of the world. It has a toString method, which converts the world back to a printable string (similar to the plan it was based on) so that we can see what’s going on inside. The world object also has a turn method, which allows all the critters in it to take one turn and updates the world to reflect their actions.

Rappresentare lo spazio
The grid that models the world has a fixed width and height. Squares are identified by their x– and y-coordinates. We use a simple type, Vector (as seen in the exercises for the previous chapter), to represent these coordinate pairs.

function Vector(x, y) {
  this.x = x;
  this.y = y;
}
Vector.prototype.plus = function(other) {
  return new Vector(this.x + other.x, this.y + other.y);
};

Next, we need an object type that models the grid itself. A grid is part of a world, but we are making it a separate object (which will be a property of a world object) to keep the world object itself simple. The world should concern itself with world-related things, and the grid should concern itself with grid-related things.

To store a grid of values, we have several options. We can use an array of row arrays and use two property accesses to get to a specific square, like this:

var grid = [["top left",    "top middle",    "top right"],
            ["bottom left", "bottom middle", "bottom right"]];
console.log(grid[1][2]); //ovviamente: bottom right

Or we can use a single array, with size width × height, and decide that the element at (x,y) is found at position x + (y × width) in the array.

var grid = ["top left",    "top middle",    "top right",
            "bottom left", "bottom middle", "bottom right"];
console.log(grid[2 + (1 * 3)]); //ovviamente: bottom right

Since the actual access to this array will be wrapped in methods on the grid object type, it doesn’t matter to outside code which approach we take. I chose the second representation because it makes it much easier to create the array. When calling the Array constructor with a single number as an argument, it creates a new empty array of the given length.

This code defines the Grid object, with some basic methods:

function Grid(width, height) {
  this.space = new Array(width * height);
  this.width = width;
  this.height = height;
}
Grid.prototype.isInside = function(vector) {
  return vector.x >= 0 && vector.x < this.width
         && vector.y >= 0 && vector.y < this.height;
};
Grid.prototype.get = function(vector) {
  return this.space[vector.x + this.width * vector.y];
};
Grid.prototype.set = function(vector, value) {
  this.space[vector.x + this.width * vector.y] = value;
};

And here is a trivial test (metto il codice che serve nel file el0.js):

function Vector(x, y) {
  this.x = x;
  this.y = y;
}
Vector.prototype.plus = function(other) {
  return new Vector(this.x + other.x, this.y + other.y);
};


function Grid(width, height) {
  this.space = new Array(width * height);
  this.width = width;
  this.height = height;
}
Grid.prototype.isInside = function(vector) {
  return vector.x >= 0 && vector.x < this.width
              && vector.y >= 0 && vector.y < this.height;
};
Grid.prototype.get = function(vector) {
  return this.space[vector.x + this.width * vector.y];
};
Grid.prototype.set = function(vector, value) {
  this.space[vector.x + this.width * vector.y] = value;
};

var grid = new Grid(5, 5);
console.log(grid.get(new Vector(1, 1)));

grid.set(new Vector(1, 1), "X");
console.log(grid.get(new Vector(1, 1)));

L’interfaccia per l’oggetto critter
Before we can start on the World constructor, we must get more specific about the critter objects that will be living inside it. I mentioned that the world will ask the critters what actions they want to take. This works as follows: each critter object has an act method that, when called, returns an action. An action is an object with a type property, which names the type of action the critter wants to take, for example "move". The action may also contain extra information, such as the direction the critter wants to move in.

Critters are terribly myopic and can see only the squares directly around them on the grid. But even this limited vision can be useful when deciding which action to take. When the act method is called, it is given a view object that allows the critter to inspect its surroundings. We name the eight surrounding squares by their compass directions: "n" for north, "ne" for northeast, and so on. Here’s the object we will use to map from direction names to coordinate offsets:

var directions = {
  "n":  new Vector( 0, -1),
  "ne": new Vector( 1, -1),
  "e":  new Vector( 1,  0),
  "se": new Vector( 1,  1),
  "s":  new Vector( 0,  1),
  "sw": new Vector(-1,  1),
  "w":  new Vector(-1,  0),
  "nw": new Vector(-1, -1)
};

The view object has a method look, which takes a direction and returns a character, for example "#" when there is a wall in that direction, or " " (space) when there is nothing there. The object also provides the convenient methods find and findAll. Both take a map character as an argument. The first returns a direction in which the character can be found next to the critter or returns null if no such direction exists. The second returns an array containing all directions with that character. For example, a creature sitting left (west) of a wall will get ["ne", "e", "se"] when calling findAll on its view object with the "#" character as argument.

Here is a simple, stupid critter that just follows its nose until it hits an obstacle and then bounces off in a random open direction:

function randomElement(array) {
  return array[Math.floor(Math.random() * array.length)];
}

var directionNames = "n ne e se s sw w nw".split(" ");

function BouncingCritter() {
  this.direction = randomElement(directionNames);
};

BouncingCritter.prototype.act = function(view) {
  if (view.look(this.direction) != " ")
    this.direction = view.find(" ") || "s";
  return {type: "move", direction: this.direction};
};

The randomElement helper function simply picks a random element from an array, using Math.random plus some arithmetic to get a random index. We’ll use this again later because randomness can be useful in simulations.

To pick a random direction, the BouncingCritter constructor calls randomElement on an array of direction names. We could also have used Object.keys to get this array from the directions object we defined earlier, but that provides no guarantees about the order in which the properties are listed. In most situations, modern JavaScript engines will return properties in the order they were defined, but they are not required to.

The “|| "s"” in the act method is there to prevent this.direction from getting the value null if the critter is somehow trapped with no empty space around it (for example when crowded into a corner by other critters).

Uhmmm… storia lunga; pausa poi continuerà 😊

:mrgreen:

Posta un commento o usa questo indirizzo per il trackback.

Trackback

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo di WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google photo

Stai commentando usando il tuo account Google. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.

%d blogger hanno fatto clic su Mi Piace per questo: