forked from hectorip/Eloquent-JavaScript-es
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path07_elife.txt
More file actions
916 lines (713 loc) · 46.1 KB
/
07_elife.txt
File metadata and controls
916 lines (713 loc) · 46.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
:chap_num: 7
:prev_link: 06_object
:next_link: 08_error
:load_files: ["code/chapter/07_elife.js", "code/animateworld.js"]
:zip: html
= Proyecto: Vida Electrónica =
[chapterquote="true"]
[quote, Edsger Dijkstra, The Threats to Computing Science]
____
[...] La pregunta de si las máquinas pueden pensar [...] es tan relevante como la pregunta de si los submarinos pueden nadar.
____
(((artificial intelligence)))(((Dijkstra+++,+++ Edsger)))(((project
chapter)))(((reading code)))(((writing code)))In “project” chapters,
En los capítulos de proyecto, dejaré de abrumarte con teoría nueva por un breve momento, y en lugar de eso trabajaremos a través de un programa juntos. La teoría es indispensable cuando aprendemos a programar, pero debería ser acompañada de lecturas y la comprensión de programas no triviales.
(((artificial life)))(((electronic life)))(((ecosystem)))Nuestro proyecto en este capítulo es construir un ecosistema virtual, un mundo pequeño poblado con bichos que se mueven alrededor y luchan por sobrevivir.
== Definición ==
(((dimensions)))(((electronic life)))Para hacer esta tarea manejable, nosotros simplificaremos radicalmente el concepto de mundo (_((world))_). Es decir un mundo será una cuadricula de dos dimensiones donde cada entidad ocupa un cuadro completo de ella. En cada _((turn))_, todos los bichos tienen oportunidad de hacer alguna acción.
(((discretization)))(((simulation)))Por lo tanto, cortamos ambos tiempo y espacio en dos unidades con un tamaño fijo: cuadros para espacio y turnos para tiempo. Por supuesto, esto es una burda e imprecisa ((aproximación)). Pero nuestra simulación pretende ser entretenida, no precisa, así que podemos acortar libremente las dimensiones.
[[plan]]
(((array)))Podemos definir un mundo como un _plan_, una matriz de cadenas que establece la cuadrícula del mundo usando un carácter por cuadro. .
// include_code
[source,javascript]
----
var plan = ["############################",
"# # # o ##",
"# #",
"# ##### #",
"## # # ## #",
"### ## # #",
"# ### # #",
"# #### #",
"# ## o #",
"# o # o ### #",
"# # #",
"############################"];
----
El carácter "#" en este programa representa paredes y rocas, y el carácter "o" representa bichos (_critters_). Los espacios, como posiblemente habrás adivinado, son espacios vacíos.
(((object)))(((toString method)))(((turn)))Una matriz unidimensional puede ser usada para crear un objeto mundo (_world_). Tal objeto mantiene seguimiento del tamaño y el contenido. El mundo tiene un método _toString_ que convierte al mundo nuevamente en una cadena imprimible (parecida al programa en el que se basó) de manera que podamos ver qué es lo que está pasando dentro. El objeto mundo también tiene un método _turn_, el cual permite a todos los bichos que lo habitan tomar un turno y luego actualizar el mundo reflejando sus acciones.
[[grid]]
== Representando el espacio. ==
(((array,as grid)))(((Vector type)))(((coordinates)))
La cuadrícula (_grid_) que modela el mundo tiene un ancho y altura fija. Los cuadros son identificados por sus coordenadas "X" y "Y". Usamos un tipo sencillo, _Vector_ (como los vistos en los ejercicios del link:06_object.html#exercise_vector[capítulo anterior]), para representar estas coordenadas en pares.
// include_code
[source,javascript]
----
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);
};
----
(((object)))(((encapsulation))) A continuacion, necesitamos un tipo de objeto que modele por si mismo la cuadricula (_grid_). La cuadricula es parte del mundo, pero nosotros estamos haciendo la cuadricula como un objeto separado (que sera una propiedad del objeto mundo) para mantener el objeto _world_ simple. El mundo debe ocuparse de las cosas relacionadas con el mundo, y la cuadricula debe ocuparse de las cosas relacionadas con la cuadricula.
(((array)))(((data structure)))Para almacenar una cuadricula de valores, tenemos varias opciones. Podemos utilizar una matriz de matrices de filas (_array of row arrays_) y utilizar dos propiedades de acceso para llegar a una cruadricula específica:
[source,javascript]
----
var grid = [["top left", "top middle", "top right"],
["bottom left", "bottom middle", "bottom right"]];
console.log(grid[1][2]);
// → bottom right
----
(((array,indexing)))(((coordinates)))(((grid)))O podemos utilizar una sola matriz, con el tamaño de ancho x alto, y decidir que el elemento en (_x_,_y_) se encuentra en la posición _x_ + (_y_ x ancho) de la matriz.
[source,javascript]
----
var grid = ["top left", "top middle", "top right",
"bottom left", "bottom middle", "bottom right"];
console.log(grid[2 + (1 * 3)]);
// → bottom right
----
(((encapsulation)))(((abstraction)))(((Array constructor)))(((array,creation)))(((array,length
of)))Dado que el acceso real a esta matriz (_Array_) será envuelto en métodos en el objeto de tipo cuadricula, no le importa al código externo cual enfoque tomamos. Elegí la segunda representación, ya que hace que sea mucho más fácil crear la matriz. Al llamar al constructor de _Array_ con un solo número como argumento, se crea una nueva matriz vacía de la longitud dada.
(((Grid type)))Este código define el objeto cuadricula (_Grid_) con algunos métodos básicos:
// include_code
[source,javascript]
----
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;
};
----
Y aquí es una prueba trivial:
[source,javascript]
----
var grid = new Grid(5, 5);
console.log(grid.get(new Vector(1, 1)));
// → undefined
grid.set(new Vector(1, 1), "X");
console.log(grid.get(new Vector(1, 1)));
// → X
----
== Una interfaz para programar bichos (_critter_) ==
(((record)))(((electronic life)))(((interface)))Antes de que podamos comenzar con nuestro _constructor_ en nuestro mundo, debemos obtener más especificaciones sobre los objetos _critter_ que estarán viviendo dentro de él. Mencioné que el mundo preguntará a las criaturas qué acciones quieren tomar. Esto funciona de esta manera: cada objeto _critter_ tiene un método _act_ que, cuando se lo llama, devuelve una acción. Una acción es un objeto con una propiedad de tipo (_type_), que indica el tipo de acción que el _critter_ quiere tomar, por ejemplo "mover". La acción también puede contener información adicional, como la dirección en la que el _critter_ quiere moverse.
[[directions]]
(((Vector type)))(((View type)))(((directions object)))(((object,as map)))Los _Critters_ son terriblemente miopes y pueden ver solamente los cuadrados directamente alrededor de ellos en la cuadricula. Pero incluso esta visión limitada puede ser útil al decidir qué acción tomar. Cuando se llama al método _act_, se recibe un objeto _view_ que permite al _critter_ inspeccionar su entorno. Nombramos los ocho cuadrados circundantes por sus direcciones de la brújula: "_n_" para el norte, "_ne_" para el noreste, y así sucesivamente. Este es el objeto que usaremos para asignar los nombres de dirección a los desplazamientos de coordenadas:
// include_code
[source,javascript]
----
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)
};
----
(((View type)))El objeto _view_ tiene un método _look_, que toma una dirección y devuelve un carácter, por ejemplo "_#_" cuando hay una pared en esa dirección, o " " (espacio) cuando no hay nada allí. El objeto también proporciona los métodos convenientes _find_ y _findAll_. Ambos toman un carácter de mapa como argumento. El primero devuelve una dirección en la que el carácter se puede encontrar con respecto al _critter_ o devuelve _null_ si no existe tal dirección. El segundo devuelve una matriz que contiene todas las direcciones con ese carácter. Por ejemplo, una criatura sentada a la izquierda (al oeste) de una pared obtendrá ["_ne_", "_e_", "_se_"] al llamar a _findAll_ en su objeto de vista con el carácter "_#_" como argumento.
(((bouncing)))(((behavior)))(((BouncingCritter type)))Aquí está un _critter_ simple y estúpido que sigue su nariz hasta que golpea un obstáculo y luego rebota en una dirección al azar:
// include_code
[source,javascript]
----
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};
};
----
(((random number)))(((Math.random function)))(((randomElement
function)))(((array,indexing)))La función de ayuda _randomElement_ simplemente selecciona un elemento aleatorio de una matriz (_Array_), usando _Math.random_ más algún cálculo aritmético obtendremos un índice aleatorio. Esto lo usaremos más adelante porque la aleatoriedad puede ser útil en simulaciones.
(((Object.keys function)))Para escoger una dirección aleatoria, el constructor _BouncingCritter_ llama a _randomElement_ en una matriz _directionNames_. También podríamos haber usado _Object.keys_ para obtener esta matriz del objeto _directions_ que definimos anteriormente, pero eso no proporciona garantías sobre el orden en el que se enumeran las propiedades. En la mayoría de las situaciones, los motores de JavaScript modernos devolverán las propiedades en el orden en que se definieron, pero no necesariamente.
(((|| operator)))(((null)))El “++|| "S"++” en el método _act_ está ahí para evitar que _this.direction_ de valor _null_ si el _critter_ está de alguna manera atrapado sin espacio vacío alrededor de él (por ejemplo, cuando se aglomeran en una esquina con otros _critters_).
== El objeto del mundo ==
(((World type)))(((electronic life)))Ahora podemos comenzar con el tipo de objeto _World_. El constructor toma un plan (la matriz de cadenas que representa la cuadrícula del mundo, link:07_elife.html#grid[descrita] anteriormente) y una leyenda (_legend_) como argumentos. Una leyenda es un objeto que nos dice qué significa cada carácter en el mapa. Contiene un constructor para cada carácter, excepto el carácter de espacio, que siempre refiere a _null_ y es el valor que usaremos para representar el espacio vacío.
// include_code
[source,javascript]
----
function elementFromChar(legend, ch) {
if (ch == " ")
return null;
var element = new legend[ch]();
element.originChar = ch;
return element;
}
function World(map, legend) {
var grid = new Grid(map[0].length, map.length);
this.grid = grid;
this.legend = legend;
map.forEach(function(line, y) {
for (var x = 0; x < line.length; x++)
grid.set(new Vector(x, y),
elementFromChar(legend, line[x]));
});
}
----
(((elementFromChar function)))(((object,as map)))En _elementFromChar_, primero creamos una _instance_ del tipo correcto buscando el constructor del carácter y aplicándole _new_. A continuación, agregamos una propiedad _originChar_ a ella para que sea fácil averiguar de qué carácter se creó el elemento originalmente.
(((toString method)))(((nesting,of loops)))(((for
loop)))(((coordinates)))Necesitamos esta propiedad _originChar_ al implementar el método _toString_ de _word_. Este método construye una cadena _maplike_ del estado actual del mundo realizando un bucle bidimensional sobre los cuadros de la cuadrícula.
// include_code
[source,javascript]
----
function charFromElement(element) {
if (element == null)
return " ";
else
return element.originChar;
}
World.prototype.toString = function() {
var output = "";
for (var y = 0; y < this.grid.height; y++) {
for (var x = 0; x < this.grid.width; x++) {
var element = this.grid.get(new Vector(x, y));
output += charFromElement(element);
}
output += "\n";
}
return output;
};
----
(((electronic life)))(((constructor)))(((Wall type)))Una pared es un objeto simple: se usa sólo para ocupar espacio y no tiene método _act_.
// include_code
[source,javascript]
----
function Wall() {}
----
(((World type)))Cuando probamos el objeto _World_ creando una instancia basada en el plan del link:07_elife.html#plan[capítulo anterior] y luego invocando _toString_ sobre él, obtenemos una cadena muy similar al plan.
// include_code strip_log
// test: trim
[source,javascript]
----
var world = new World(plan, {"#": Wall,
"o": BouncingCritter});
console.log(world.toString());
// → ############################
// # # # o ##
// # #
// # ##### #
// ## # # ## #
// ### ## # #
// # ### # #
// # #### #
// # ## o #
// # o # o ### #
// # # #
// ############################
----
== _This_ y su alcance ==
(((forEach
method)))(((function,scope)))(((this)))(((scope)))(((self
variable)))(((global object)))El constructor _World_ contiene una llamada a _forEach_. Una cosa interesante a notar es que dentro de la función pasada a _forEach_ ya no estamos directamente en el ámbito de funciones del _constructor_. Cada llamada de función obtiene su propia vinculación para _this_, por lo que el _this_ en la función interna no se refiere al mismo objeto en la nueva construcción que se refiere _this_ en la funcion externa. De hecho, cuando una función no se llama como un (método), _this_ hará referencia al objeto global.
Esto significa que no podemos escribir _this.grid_ para acceder a la cuadrícula desde dentro del bucle. En su lugar, la función externa crea una variable local normal, _grid_, a través de la cual la función interna obtiene acceso a la cuadrícula.
(((future)))(((ECMAScript 6)))(((arrow function)))(((self
variable)))Esto es un poco un error de diseño en JavaScript. Afortunadamente, la siguiente versión del lenguaje proporciona una solución para este problema. Mientras tanto, hay soluciones. Un patrón común es decir _var self = this_ y a partir de entonces se refiere a _self_, que es una variable normal y, por lo tanto, visible a las funciones internas.
(((bind method)))(((this)))Otra solución es utilizar el método _bind_, que nos permite proporcionar un objeto explícito a enlazar.
[source,javascript]
----
var test = {
prop: 10,
addPropTo: function(array) {
return array.map(function(elt) {
return this.prop + elt;
}.bind(this));
}
};
console.log(test.addPropTo([5]));
// → [15]
----
(((map method)))La función pasada al _map_ es el resultado de la llamada de enlace (_bind call_) y por lo tanto _this_ está vinculado al primer argumento dado a _bind_, el valor _this_ de la función externa (que contiene el objeto de prueba).
(((context parameter)))(((function,higher-order)))La mayoría de los métodos estándar de orden superior en matrices (_arrays_), como _forEach_ y _map_, toman un segundo argumento opcional que también se puede utilizar para proporcionar un _this_ para las llamadas a la función de iteración. Así que podría expresar el ejemplo anterior de una manera un poco más simple.
[source,javascript]
----
var test = {
prop: 10,
addPropTo: function(array) {
return array.map(function(elt) {
return this.prop + elt;
}, this); // ← no bind
}
};
console.log(test.addPropTo([5]));
// → [15]
----
Esto sólo funciona para las funciones de orden superior que soportan dicho parámetro de contexto. Cuando no lo hacen, tendrá que utilizar uno de los otros enfoques.
(((context parameter)))(((function,higher-order)))(((call method)))En nuestras propias funciones de orden superior, podemos utilizar este parámetro de contexto utilizando el método _call_ para llamar a la función dada como argumento. Por ejemplo, aquí hay un método _forEach_ para nuestro tipo _Grid_, que llama a una función dada para cada elemento de la cuadrícula que no es _null_ o _undefined_:
// include_code
[source,javascript]
----
Grid.prototype.forEach = function(f, context) {
for (var y = 0; y < this.height; y++) {
for (var x = 0; x < this.width; x++) {
var value = this.space[x + y * this.width];
if (value != null)
f.call(context, value, new Vector(x, y));
}
}
};
----
== Animando la vida ==
(((simulation)))(((electronic life)))(((World type)))El siguiente paso es escribir un método de _turn_ para el objeto _word_ que da a los bichos la oportunidad de actuar. Recorrerá la cuadrícula usando el método _forEach_ que acabamos de definir, buscando objetos con un método _act_. Cuando encuentre uno, _turn_ llamadra dicho método _act_ para obtener un objeto y llevar a cabo ese tipo de acción. Por ahora, solo se entienden las acciones tipo "mover".
(((grid)))Hay un problema potencial con este enfoque. ¿Puedes detectarlo? Si dejamos que los bichos se muevan al cruzarlos, pueden moverse a un cuadrado que todavía no hemos visto, y les permitiremos moverse de nuevo cuando alcancemos ese cuadrado. Por lo tanto, tenemos que mantener una serie de criaturas que ya han tenido su turno e ignorarlos cuando los vemos de nuevo.
// include_code
[source,javascript]
----
World.prototype.turn = function() {
var acted = [];
this.grid.forEach(function(critter, vector) {
if (critter.act && acted.indexOf(critter) == -1) {
acted.push(critter);
this.letAct(critter, vector);
}
}, this);
};
----
(((this)))Utilizamos el segundo parámetro del método _forEach_ de la cuadrícula para poder acceder al _this_ correcto dentro de la función interna. El método _letAct_ contiene la lógica real que permite a los _critters_ moverse.
// include_code
[[checkDestination]]
[source,javascript]
----
World.prototype.letAct = function(critter, vector) {
var action = critter.act(new View(this, vector));
if (action && action.type == "move") {
var dest = this.checkDestination(action, vector);
if (dest && this.grid.get(dest) == null) {
this.grid.set(vector, null);
this.grid.set(dest, critter);
}
}
};
World.prototype.checkDestination = function(action, vector) {
if (directions.hasOwnProperty(action.direction)) {
var dest = vector.plus(directions[action.direction]);
if (this.grid.isInside(dest))
return dest;
}
};
----
(((View type)))(((electronic life)))Primero, pedimos simplemente al _critter_ que actúe, pasándola un objeto _view_ con datos sobre el mundo y la posición actual de la criatura (definiremos _view_ en un link:07_elife.html#view[momento]). El método _act_ devuelve una acción de algún tipo.
Si el tipo (_type_) de la acción no es "mover", se ignora. Si es "mover", si tiene una propiedad de dirección que se refiere a una dirección válida, y si el cuadrado en esa dirección es vacío (_null_), fijamos el cuadrado donde el _critter_ estaba como _null_ y almacenamos el critter en el cuadrado de destino.
(((error tolerance)))(((defensive programming)))(((sloppy
programming)))(((validation)))Tenga en cuenta que _letAct_ se encarga de ignorar la entrada no valida, no asume que la propiedad _direction_ de la acción es válida o que la propiedad _type_ tiene sentido. Este tipo de (programación defensiva) tiene sentido en algunas situaciones. La principal razón para hacerlo es validar entradas procedentes de fuentes que no controla (como la entrada de usuario o de archivo), pero también puede ser útil para aislar subsistemas entre sí. En este caso, la intención es que los _critters_ puedan ser programados descuidadamente (no tienen que verificar si sus acciones previstas tienen sentido. Pueden solicitar una acción, y el mundo averiguará si permitirla).
(((interface)))(((private property)))(((access
control)))(((property,naming)))(((underscore character)))(((World
type)))Estos dos métodos no forman parte de la interfaz externa de un objeto _World_. Ellos son un detalle interno. Algunos lenguajes proporcionan formas de declarar explícitamente ciertos métodos y propiedades privadas y señalan un error cuando intenta utilizarlos desde fuera del objeto. JavaScript no, por lo que tendrá que depender de alguna otra forma de comunicación para describir lo que es parte de la interfaz de un objeto. A veces puede ayudar utilizar un esquema de nomenclatura para distinguir entre propiedades externas e internas, por ejemplo prefijando todas las internas con un carácter de subrayado (_). Esto hará que los usos accidentales de las propiedades que no sean parte de la interfaz de un objeto sean más fáciles de detectar.
[[view]]
(((View type)))La parte que falta, el tipo _view_, se parece a esto:
// include_code
[source,javascript]
----
function View(world, vector) {
this.world = world;
this.vector = vector;
}
View.prototype.look = function(dir) {
var target = this.vector.plus(directions[dir]);
if (this.world.grid.isInside(target))
return charFromElement(this.world.grid.get(target));
else
return "#";
};
View.prototype.findAll = function(ch) {
var found = [];
for (var dir in directions)
if (this.look(dir) == ch)
found.push(dir);
return found;
};
View.prototype.find = function(ch) {
var found = this.findAll(ch);
if (found.length == 0) return null;
return randomElement(found);
};
----
(((defensive programming)))El método _look_ calcula las coordenadas que estamos tratando de ver y, si están dentro de la cuadrícula, encuentra el carácter correspondiente al elemento que se encuentra allí. Para las coordenadas fuera de la cuadrícula, _look_ simplemente finge que hay una pared allí de modo que si usted define un mundo sin paredes, las criaturasno serán tentadas de intentar caminar fuera de los bordes.
== Se mueve ==
(((electronic life)))(((simulation)))Hemos instanciado un objeto del mundo anterior. Ahora que hemos añadido todos los métodos necesarios, debería ser posible hacer que el mundo se mueva.
[source,javascript]
----
for (var i = 0; i < 5; i++) {
world.turn();
console.log(world.toString());
}
// → … five turns of moving critters
----
ifdef::book_target[]
Los primeros dos mapas que se muestran será algo similar a esto:
----
############################ ############################
# # # ## # # # ##
# o # # #
# ##### # # ##### o #
## # # ## # ## # # ## #
### ## # # ### ## # #
# ### # # # ### # #
# #### # # #### #
# ## # # ## #
# # o ### # #o # ### #
#o # o # # # o o #
############################ ############################
----
(((animation))) ¡Ellos mueven! Para obtener una visión más interactiva de estas
criaturas que se arrastran y rebotan en las paredes, abra este capítulo en la versión on-line del libro http://eloquentjavascript.net[_eloquentjavascript.net_].
endif::book_target[]
ifdef::interactive_target[]
Sin embargo, imprimir muchas copias del mapa es una manera bastante desagradable de observar un mundo. Es por eso que el _sandbox_ proporciona una función _animateWorld_ que ejecutará un mundo como una animación en pantalla, moviéndose tres veces por segundo, hasta que pulse el botón de parada.
// test: no
[source,javascript]
----
animateWorld(world);
// → … life!
----
La implementación de _animateWorld_ seguirá siendo un misterio por ahora, pero después de haber leído los link:13_dom.html#dom[capítulos posteriores] de este libro, y estudiar la integración de JavaScript en los navegadores web, ya no se verá tan mágico.
endif::interactive_target[]
== Más formas de vida ==
El punto culminante dramático de nuestro mundo, si usted mira para un pedacito, es cuando dos _critters_ rebotan el uno al otro. ¿Puedes pensar en otra forma interesante de ((comportamiento))?
(((wall following)))Podemos hacer un _critter_ que se mueve a lo largo de las paredes (_WallFollower_). Conceptualmente, el critter mantiene su mano izquierda (pata, tentáculo, lo que sea) a la pared y sigue adelante. Este no es un cambio trivial de implementar:
(((WallFollower type)))(((directions object))) Tenemos que ser capaces de “hacer calculos" con las ((direcciones de la brújula)). Como las direcciones son modeladas por un conjunto de cadenas, deberíamos definir nuestra propia operación (_dirPlus_) para calcular direcciones relativas. Así dirPlus ("_n_", _1_) significa una vuelta de 45 grados en sentido horario desde el norte, dando "_ne_". Del mismo modo, dirPlus ("_s_", _-2_) significa 90 grados en sentido antihorario desde el sur, que es el este.
// include_code
[source,javascript]
----
function dirPlus(dir, n) {
var index = directionNames.indexOf(dir);
return directionNames[(index + n + 8) % 8];
}
function WallFollower() {
this.dir = "s";
}
WallFollower.prototype.act = function(view) {
var start = this.dir;
if (view.look(dirPlus(this.dir, -3)) != " ")
start = this.dir = dirPlus(this.dir, -2);
while (view.look(this.dir) != " ") {
this.dir = dirPlus(this.dir, 1);
if (this.dir == start) break;
}
return {type: "move", direction: this.dir};
};
----
(((artificial intelligence)))(((pathfinding)))(((View type)))El método _act_ sólo tiene que "escanear" el entorno del _critter_, comenzando por su lado izquierdo y en sentido horario hasta encontrar un cuadrado vacío. Entonces se mueve en la dirección de ese cuadrado vacío.
Lo que complica las cosas es que un _critter_ puede terminar en el medio del espacio vacío, ya sea como su posición inicial o como resultado de caminar alrededor de otro _critter_. Si aplicamos el enfoque que acabo de describir en el espacio vacío, el pobre _critter_ seguirá girando a la izquierda en cada paso, corriendo en círculos.
De modo que hay una comprobación extra (la sentencia _if_) para iniciar el escaneado hacia la izquierda sólo si parece que el _critter_ acaba de pasar algún tipo de obstáculo, es decir, si el espacio detrás y hacia la izquierda del _critter_ no está vacío. De lo contrario, el _critter_ comienza a escanear directamente delante, de modo que va a caminar recto en el espacio vacío.
(((infinite loop)))Y finalmente, hay una prueba que compara _this.dir_ con _start_ después de cada paso a través del _loop_ para cerciorarse de que el _loop_ no funcionará para siempre cuando el _critter_ está amurallado adentro o apretado adentro por otros _critters_ y no puede encontrar un cuadrado vacío.
ifdef::interactive_target[]
Este pequeño mundo muestra las criaturas que siguen la pared:
// test: no
[source,javascript]
----
animateWorld(new World(
["############",
"# # #",
"# ~ ~ #",
"# ## #",
"# ## o####",
"# #",
"############"],
{"#": Wall,
"~": WallFollower,
"o": BouncingCritter}
));
----
endif::interactive_target[]
== Una simulación más realista ==
(((simulation)))(((electronic life)))Para hacer la vida en nuestro mundo más interesante, vamos a añadir los conceptos de la comida y la reproducción. Cada ser vivo en el mundo recibe una nueva propiedad, la energía, que se reduce mediante la realización de acciones y el aumento de comer cosas. Cuando la criatura tiene suficiente energía, puede reproducirse, generando una nueva criatura del mismo tipo. Para mantener las cosas simples, las criaturas en nuestro mundo se reproducen asexualmente por sí solas.
Si los bichos sólo se mueven y comen unos a otros, el mundo pronto sucumbirá a la ley de entropía creciente, se quedará sin energía, y se convertirá en un desierto sin vida. Para evitar que esto suceda (demasiado rápido, al menos), añadimos plantas al mundo. Las plantas no se mueven. Simplemente utilizan la fotosíntesis para crecer (es decir, aumentar su energía) y reproducirse.
(((World type)))Para que esto funcione, necesitaremos un mundo con un método _letAct_ diferente. Podríamos simplemente reemplazar el método del (prototipo _world_), pero me he vuelto muy apegado a nuestra simulación con las criaturas que siguen a la pared y odiaría romper ese viejo mundo.
(((actionTypes object)))(((LifeLikeWorld type)))Una solución es usar la (herencia). Creamos un nuevo (constructor), _LifelikeWorld_, cuyo prototipo se basa en el (prototipo World) pero que anula el método _letAct_. El nuevo método _letAct_ delega el trabajo de realizar una acción a varias funciones almacenadas en el objeto _actionTypes_.
// include_code
[source,javascript]
----
function LifelikeWorld(map, legend) {
World.call(this, map, legend);
}
LifelikeWorld.prototype = Object.create(World.prototype);
var actionTypes = Object.create(null);
LifelikeWorld.prototype.letAct = function(critter, vector) {
var action = critter.act(new View(this, vector));
var handled = action &&
action.type in actionTypes &&
actionTypes[action.type].call(this, critter,
vector, action);
if (!handled) {
critter.energy -= 0.2;
if (critter.energy <= 0)
this.grid.set(vector, null);
}
};
----
(((electronic life)))(((function,as value)))(((call method)))(((this)))El nuevo método _letAct_ comprueba primero si se ha devuelto una acción, si existe una (función de controlador) para este tipo de acción y, por último, si ese controlador devuelve _true_, lo que indica que se ha tratado correctamente la acción.
Tenga en cuenta el uso de _call_ para dar al manejador acceso al mundo a través de _this_.
Si la acción no funcionó por alguna razón, la acción predeterminada es que la criatura simplemente espere. Pierde un quinto punto de energía, y si su nivel de energía es igual o menor que cero, la criatura muere y se borra de la cuadrícula.
== Manejadores de acciones ==
(((photosynthesis)))La acción más simple que una criatura puede realizar es "crecer" (_grow_), usada por las plantas. Cuando se devuelve un objeto de acción como _{type: "grow"}_, se llamará al siguiente método del controlador:
// include_code
[source,javascript]
----
actionTypes.grow = function(critter) {
critter.energy += 0.5;
return true;
};
----
Crecer siempre tiene éxito y añade medio punto al nivel de energía (_energy_) de la planta.
Moverse es más complicado.
// include_code
[source,javascript]
----
actionTypes.move = function(critter, vector, action) {
var dest = this.checkDestination(action, vector);
if (dest == null ||
critter.energy <= 1 ||
this.grid.get(dest) != null)
return false;
critter.energy -= 1;
this.grid.set(vector, null);
this.grid.set(dest, critter);
return true;
};
----
(((validation)))Esta acción comprueba primero, utilizando el método _checkDestination_ definido anteriormente, si la acción proporciona un destino válido. Si no, o si el destino no está vacío, o si el _critter_ carece de la energía requerida, _move_ devuelve false para indicar que no se tomó ninguna acción. De lo contrario, se mueve el _critter_ y resta el costo de la energía.
(((food)))Además de moverse, las criaturas pueden comer.
// include_code
[source,javascript]
----
actionTypes.eat = function(critter, vector, action) {
var dest = this.checkDestination(action, vector);
var atDest = dest != null && this.grid.get(dest);
if (!atDest || atDest.energy == null)
return false;
critter.energy += atDest.energy;
this.grid.set(dest, null);
return true;
};
----
(((validation)))Comer otra criatura (_critter_) también implica proporcionar un cuadrado de destino válido. Esta vez, el destino no debe estar vacío y debe contener algo con energía, como un _critter_ (pero no una pared - las paredes no son comestibles). Si es así, la energía de la comida es transferida al comedor, y la víctima es removida de la cuadrícula.
(((reproduction)))Y finalmente, permitimos que nuestras criaturas se reproduzcan.
// include_code
[source,javascript]
----
actionTypes.reproduce = function(critter, vector, action) {
var baby = elementFromChar(this.legend,
critter.originChar);
var dest = this.checkDestination(action, vector);
if (dest == null ||
critter.energy <= 2 * baby.energy ||
this.grid.get(dest) != null)
return false;
critter.energy -= 2 * baby.energy;
this.grid.set(dest, baby);
return true;
};
----
(((electronic life)))Reproducirse cuesta dos veces el nivel de energía de un _critter_ recién nacido. Así que primero creamos un bebé (hipotético) usando _elementFromChar_ en el propio carácter de origen del _critter_. Una vez que tenemos un bebé, podemos encontrar su nivel de energía y probar si el padre tiene suficiente energía para llevarlo con éxito al mundo. También necesitamos un destino válido (y vacío).
(((reproduction)))Si todo está bien, el bebé es puesto en la cuadrícula (ya no es hipotético), y la energía se gasta.
== Llenar el nuevo mundo ==
(((Plant type)))(((electronic life)))Ahora tenemos un marco (_framework_) para simular estas criaturas en forma más realista. Podríamos poner las criaturas del viejo mundo en ella, pero sólo morirían ya que no tienen una propiedad de energía. Así que hagamos nuevos. Primero escribiremos una planta (_Plant_), que es una forma de vida bastante simple.
// include_code
[source,javascript]
----
function Plant() {
this.energy = 3 + Math.random() * 4;
}
Plant.prototype.act = function(view) {
if (this.energy > 15) {
var space = view.find(" ");
if (space)
return {type: "reproduce", direction: space};
}
if (this.energy < 20)
return {type: "grow"};
};
----
(((reproduction)))(((photosynthesis)))(((random
number)))(((Math.random function)))Las plantas comienzan con un nivel de energía entre 3 y 7, aleatorizado para que no se reproduzcan todos en el mismo turno (_turn_). Cuando una planta alcanza 15 puntos de energía y hay un espacio vacío cerca, se reproduce en ese espacio vacío. Si una planta no puede reproducirse, simplemente crece hasta alcanzar el nivel de energía 20.
(((critter)))(((PlantEater type)))(((herbivore)))(((food chain)))Ahora definimos un comedor de plantas (_PlantEater_).
// include_code
[source,javascript]
----
function PlantEater() {
this.energy = 20;
}
PlantEater.prototype.act = function(view) {
var space = view.find(" ");
if (this.energy > 60 && space)
return {type: "reproduce", direction: space};
var plant = view.find("*");
if (plant)
return {type: "eat", direction: plant};
if (space)
return {type: "move", direction: space};
};
----
Usaremos el carácter _*_ para las plantas (_Plant_), así que eso es lo que esta criatura buscará cuando busque comida.
== Darle vida ==
(((electronic life)))Y eso nos da suficientes elementos para probar nuestro nuevo mundo. Imagínese el siguiente mapa como un valle cubierto de hierba con una manada de herbívoros en ella, algunas rocas y una exuberante vegetación en todas partes.
// include_code
[source,javascript]
----
var valley = new LifelikeWorld(
["############################",
"##### ######",
"## *** **##",
"# *##** ** O *##",
"# *** O ##** *#",
"# O ##*** #",
"# ##** #",
"# O #* #",
"#* #** O #",
"#*** ##** O **#",
"##**** ###*** *###",
"############################"],
{"#": Wall,
"O": PlantEater,
"*": Plant}
);
----
(((animation)))(((simulation)))Veamos ver ocurre cuando ejecutamos este codigo.
(!book These snapshots illustrate a typical run of this world.!)
ifdef::interactive_target[]
// start_code
// test: no
[source,javascript]
----
animateWorld(valley);
----
endif::interactive_target[]
ifdef::book_target[]
// include_code
[source,javascript]
----
############################ ############################
##### ###### ##### ** ######
## *** O *## ## ** * O ##
# *##* ** *## # **## ##
# ** ##* *# # ** O ##O #
# ##* # # *O * * ## #
# ## O # # *** ## O #
# #* O # #** #*** #
#* #** O # #** O #**** #
#* O O ##* **# #*** ##*** O #
##* ###* ### ##** ###** O ###
############################ ############################
############################ ############################
#####O O ###### ##### O ######
## ## ## ##
# ##O ## # ## O ##
# O O *## # # ## #
# O O O **## O # # ## #
# **## O # # O ## * #
# # *** * # # # O #
# # O***** O # # O # O #
# ##****** # # ## O O #
## ###****** ### ## ### O ###
############################ ############################
############################ ############################
##### ###### ##### ######
## ## ## ** * ##
# ## ## # ## ***** ##
# ## # # ##**** #
# ##* * # # ##***** #
# O ## * # # ##****** #
# # # # # ** ** #
# # # # # #
# ## # # ## #
## ### ### ## ### ###
############################ ############################
----
endif::book_target[]
(((stability)))(((reproduction)))(((extinction)))(((starvation)))La mayoría de las veces, las plantas se multiplican y se expanden con bastante rapidez, pero luego la abundancia de alimentos causa una explosión poblacional de los herbívoros, que proceden a eliminar todas o casi todas las plantas, resultando en un hambre masiva de las criaturas. A veces, el ecosistema se recupera y comienza otro ciclo. En otras ocasiones, una de las especies muere completamente. Si son los herbívoros, todo el espacio se llenará de plantas. Si son las plantas, las criaturas restantes mueren de hambre, y el valle se convierte en una tierra baldía desolada. Ah, la crueldad de la naturaleza.
== Ejercicios ==
=== Estupidez artificial ===
(((artificial stupidity (exercise))))(((artificial
intelligence)))(((extinction)))Tener extinguidos a los habitantes de nuestro mundo después de unos minutos es algo deprimente. Para hacer frente a esto, podríamos tratar de crear un comedor de plantas más inteligente (_smarter plant eater_).
(((pathfinding)))(((reproduction)))(((food)))Hay varios problemas obvios con nuestros herbívoros. En primer lugar, son terriblemente codiciosos, tragan cada planta que ven hasta que han borrado toda la vida vegetal. En segundo lugar, su movimiento al azar (recuerde que el método _view.find:_ devuelve una dirección al azar cuando coinciden varias direcciones) hace que tropiecen ineficazmente y mueren de hambre si no encuentran ninguna planta cerca. Y finalmente, se reproducen muy rápido, lo que hace que los ciclos entre la abundancia y el hambre sean bastante intensos.
Crea un nuevo tipo de _critter_ intentando abordar uno o más de estos puntos y sustituyelo por el antiguo tipo _PlantEater_ en el mundo del valle. Haga algunos ajustes mas si lo ve necesario.
ifdef::interactive_target[]
// test: no
[source,javascript]
----
// Your code here
function SmartPlantEater() {}
animateWorld(new LifelikeWorld(
["############################",
"##### ######",
"## *** **##",
"# *##** ** O *##",
"# *** O ##** *#",
"# O ##*** #",
"# ##** #",
"# O #* #",
"#* #** O #",
"#*** ##** O **#",
"##**** ###*** *###",
"############################"],
{"#": Wall,
"O": SmartPlantEater,
"*": Plant}
));
----
endif::interactive_target[]
!!pista!!
(((artificial stupidity (exercise))))(((artificial
intelligence)))(((behavior)))(((state)))El problema de la codicia puede ser atacado de varias maneras. Las criaturas podrían dejar de comer cuando alcanzan un cierto nivel de energía. O pueden comer sólo cada N vueltas (manteniendo un contador de las vueltas desde su última comida en una propiedad en el objeto de criatura). O, para asegurarse de que las plantas nunca se extingan por completo, los animales podrían negarse a comer una planta a menos que vean por lo menos una planta cercana (usando el método findAll en la vista). Una combinación de estos, o alguna estrategia totalmente diferente, también podría funcionar
El problema de la codicia puede ser atacado de varias maneras. Las criaturas podrían dejar de comer cuando alcanzan un cierto nivel de energía (_energy_). O pueden comer sólo cada _N_ vueltas (manteniendo un contador de las vueltas desde su última comida en una propiedad en el objeto _critters_). O, para asegurarse de que las plantas nunca se extingan por completo, los animales podrían negarse a comer una planta a menos que vean por lo menos una planta cercana (usando el método _findAll_ en la vista). Una combinación de estos, o alguna estrategia totalmente diferente, también podría funcionar.
(((pathfinding)))(((wall following)))Hacer que los bichos se muevan más eficazmente se podría hacer copiando una de las estrategias de movimiento de las criaturas en nuestro viejo mundo sin energías. Tanto el comportamiento de rebote (_bouncing_) como el comportamiento de seguimiento de la pared (_wall-following_) mostraron un rango de movimiento mucho más amplio que el escalonamiento completamente aleatorio.
(((reproduction)))(((stability)))Hacer que las criaturas se reproduzcan más lentamente es trivial. Sólo aumentar el nivel mínimo de energía en la que se reproducen. Por supuesto, hacer el ecosistema más estable también lo hace más aburrido. Si tienes un puñado de criaturas gordas e inmóviles reproducirse pero siempre comiendo en un mar de plantas, tienes un ecosistema muy estable, pero muy aburrido.
!!pista!!
=== Depredadores ===
(((predators (exercise))))(((carnivore)))(((food chain)))Cualquier ecosistema serio tiene una cadena alimentaria más larga que un solo eslabón. Escriba otro _critter_ que sobreviva comiendose al _critter_ herbívoro. Usted notará que la estabilidad es aún más difícil de lograr ahora que hay ciclos en múltiples niveles. Trate de encontrar una estrategia para hacer que el ecosistema funcione sin problemas durante al menos un tiempo.
(((Tiger type)))Una cosa que ayudará es hacer el mundo más grande. De esta manera, los auges o bajas de la población local son menos propensos a eliminar completamente una especie, y hay espacio para la población de presas relativamente grande que se necesita para mantener una pequeña población de depredadores.
ifdef::interactive_target[]
// test: no
[source,javascript]
----
// Your code here
function Tiger() {}
animateWorld(new LifelikeWorld(
["####################################################",
"# #### **** ###",
"# * @ ## ######## OO ##",
"# * ## O O **** *#",
"# ##* ########## *#",
"# ##*** * **** **#",
"#* ** # * *** ######### **#",
"#* ** # * # * **#",
"# ## # O # *** ######",
"#* @ # # * O # #",
"#* # ###### ** #",
"### **** *** ** #",
"# O @ O #",
"# * ## ## ## ## ### * #",
"# ** # * ##### O #",
"## ** O O # # *** *** ### ** #",
"### # ***** ****#",
"####################################################"],
{"#": Wall,
"@": Tiger,
"O": SmartPlantEater, // from previous exercise
"*": Plant}
));
----
endif::interactive_target[]
!!pista!!
(((predators (exercise))))(((reproduction)))(((starvation)))Muchos de los mismos trucos que trabajaron para el ejercicio anterior también se aplican aquí. Se recomienda hacer que los depredadores sean grandes (mucha energía) y que se reproduzcan lentamente. Eso los hará menos vulnerables a los períodos de hambre cuando los herbívoros son escasos.
Más allá de mantenerse vivo, mantener vivo su alimento es el principal objetivo de un depredador. Encuentre alguna manera de hacer que los depredadores cazan de manera más agresiva cuando hay muchos herbívoros y cazan más lentamente (o no) cuando la presa es rara. Dado que los comedores de plantas se mueven, el simple truco de comer uno solo cuando otros están cerca no es probable que funcione, eso sucederá tan rara vez que su depredador se muera de hambre. Pero usted podría seguir la pista de observaciones en vueltas anteriores, en una cierta estructura de datos guardada en los objetos del depredador, y tenerla basar su comportamiento (_behavior_) en lo que ha visto recientemente.
!!pista!!