Pixel-Evolution - Jäger und Beute ausbalanciert
Pixel-Evolution-Tutorial von Daniel Schwamm (03.04.2009)
Inhalt
Bisher habe ich zum Thema "Künstliche Intelligenz" nur ein paar alte
Texte auf der Homepage veröffentlicht. Daher wollen wir heute einmal etwas
konkreter werden und zeigen, wie man etwas in dieser Richtung selbst
programmieren kann.
Ziel ist es, nicht weniger als eine kleine Simulation des Lebens zu
entwickeln. Wir geben hierzu in einem abgeschlossenem System eine
Anzahl Jäger und Beute vor, die mit mit minimaler "Intelligenz"
ausgestattet werden. Die Jäger dezimieren "im Spiel" natürlich die Beute,
und die Beute repliziert sich in gewissen Abständen selbst.
Und unsere Rolle dabei?
Über verschiedene Steuerungs-Parameter können wir "Gott-gleich" in das
System eingreifen, und so dafür sorgen, dass Jäger und Beute in Balance
bleiben, das "Leben" dieser Welt insgesamt also möglichst lange bestehen
bleibt.
Das Programm "Pixel-Evolution" ist in Delphi 7 programmiert.
Es basiert in Teilen auf Quell-Code, den ich vor ein paar Jahren
in einen Bildschirm-Schoner integriert hatte. Der Source ist überarbeitet
und erweitert, gleichzeitig jedoch auch auf das nötigste reduziert worden.
Alles spielt sich in nur einer Unit und auf einem Formular ab.
"hauptf": Das Hauptf-Formular von "Pixel-Evolution".
Einige Programm-Parameter sind "festverdrahtet" und werden als
Konstante implementiert. Dazu gehört z.B. die maximal
mögliche Anzahl von Jägern und Beute (jeweils 5.000 Exenplare),
damit wir uns umständliche dynamische Speicher-Allokationen
ersparen und direkt mit statischen Arrays arbeiten können.
Die verwendeten Konstanten sind:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
const
_caption='Pixel-Evolution von Daniel Schwamm (Maerz 2009)';
//parameter-datei
_inifn='pixelevolution.ini';
//maximale anzahl beute und jaeger
//insgesamt kann es 2*_leben_max Viehzeug geben
_leben_max=5000;
//unbenutzte array-position
_leben_unbenutzt=-2;
//momentan freie array-position
_leben_frei=-1;
//kleinste groesse von leben in pixeln
_leben_groesse_min=5;
//minimale lebensdauer (in jahren)
//Variationsgrenze nach unten
_jaeger_lebensdauer_min=10;
//wenn diese prozent-zahl der dichte ueberschritten wird
//greift lernregel zur verringerung der lebensdauer
jaeger_lern_minus=70;
//wenn diese prozent-zahl der dichte unterschritten wird
//greift lernregel zur erhoehung der lebensdauer
jaeger_lern_plus=30;
//mininmal teildauer (in jahren)
_beute_teildauer_min=3;
//wenn diese prozent-zahl der dichte ueberschritten wird
//greift lernregel zur erhoehung der teildauer
beute_lern_plus=70;
//wenn diese prozent-zahl der dichte unterschritten wird
//greift lernregel zur verringerung der teildauer
beute_lern_minus=30;
//anzahl beute die jaeger schnappen muss zur duplizierung
_beute_fuer_duplikat=4;
//dichte-array-zeichen
_dichte_frei=' ';
_dichte_jaeger='j';
_dichte_beute='b';
Hierzu sei angemerkt: Jaeger und Beute sind bis zu einem gewissen
Grad "lernfähig". Anhand der "Dichte" können Jaeger in der
nächsten Generation ihre Lebensdauer reduzieren oder erhöhen,
je nachdem, ob Über- oder Unterbevölkerung vorliegt.
In ähnlicher Weise kann die Beute die Teildauer der nächsten
Generation modifizieren, also die Zeit, die sie lebt, bevor sie sich
selbst durch Teilung reproduziert. Wobei mit "Zeit" die Abfolge
kompletter Evolutions-Runden gemeint ist. Man kann, wenn man so will,
eine solche Runde als ein Jahr interpretieren.
Eine "jaeger_lebensdauer_min" von "10" bedeutet dann,
ein Jäger wird mindestens 10 Jahre alt. Bekommt er innerhalb
dieser Zeit nicht die nötige Beute zu fassen (hier
"_beute_fuer_duplikat=4"), stirbt er. Ansonsten repliziert
sich das Tier und erhält die für die aktuelle Generation der Jäger
geltende Lebensdauer zurück (plus/minus einer gewissen
Variations-Dauer).
Eine "beute_teildauer_min" von "3" bewirkt hingegen,
dass sich ein Beute-Tier nach frühestens 3 Jahren durch Teilung
fortpflanzen kann - es sei denn, es wurde zuvor von einem Jäger erwischt.
Für unser Modell gilt dabei vereinfacht: Gäbe es keine Jäger, würde die
Anzahl der Beute stetig anwachsen. Eine Selbstregulierung der
Nachkommenschaft durch knapperes Nahrungsangebot oder ähnliches gibt
es nicht.
Für mich einigermassen überraschend war, dass die sich die besten
Grenzwerte für die Lebens- und Teildauer für ein ausbalanciertes Verhältnis
zwischen Jäger und Beute tatsächlich einigermassen mit denen der
wahren Natur decken. Auch hier haben z.B. Jäger im Schnitt eine
längere Lebenserwartung als ihre Beute-Tiere, während diese dagegen deutlich
fortpflanzungsfreudiger sind.
Das spricht bis zu einem gewissen Grad für die Gültigkeit des Modells :-)
Jäger und Beute sind vom Typ "TLeben".
00001
00002
00003
00004
00005
00006
00007
00008
//lebenseinheit: entweder jaeger oder beute
TLeben=record
x:smallint; //x-koordinate der position
y:smallint; //y-koordinate der position
zaehler:smallint; //lebensdauer, wenn jaeger
//teildauer, wenn beute
gefressen:smallint; //gefressene beute, wenn jaeger
end;
Eine Lebensform zeichnet sich dadurch aus, dass sie sich an
einem bestimmten Ort befindet, repräsentiert durch die Attribute
"x" und "y". Wie wir später sehen werden, werden
die Koordinaten "gerastert", wodurch sich das Leben auf einer
Art Gitter-Netz bewegt.
Jeder Jäger verfügt über eine "genetisch" vorprogrammierte
Lebensdauer, repräsentiert durch einen "zaehler",
der in jeder Evolutions-Runde runtergezählt wird. Ist der Zähler
Null, stirbt das Tier.
Beute verfügt dagegen über eine vorgegebene "Teildauer",
die ebenfalls in "zaehler" wiedergegeben wird. Erreicht
der Zähler den Wert Null, stirbt das Tier nicht,
sondern es dupliziert sich vielmehr durch Teilung.
Nur bei Jägern wird das Attribut "gefressen"
berücksichtigt. Hier wird die Zahl der Beute-Tiere aufsummiert,
die der Jäger verspeist hat. Ist eine gewisse Anzahl erreicht,
kann sich auch der Jäger duplizieren.
Die Gesamtheit der Jäger und Beute in "Pixel-Evolution" werden
jeweils in einem ein-dimensionalen "TLeben"-Array verwaltet,
welche in der Klasse "thauptf" deklariert sind.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
Thauptf = class(TForm)
optionen_p: TPanel;
startb: TButton;
Label1: TLabel;
[...]
public
{ Public-Deklarationen }
//heimat-verzeichnis
homedir:string;
//puffer-bitmap
bmp:tbitmap;
//jaeger- und beute-array
Jaeger_ar:array[0.._leben_max]of TLeben;
Beute_ar:array[0.._leben_max]of TLeben;
//anzahl aktive jaeger, aktive beute
jaeger_zaehler,beute_zaehler:smallint;
//dichte-array
dichte_ar:array[0.._leben_max,0.._leben_max]of char;
//variablen fuer die grafik-ausgabe
raster_rand_breite,raster_rand_hoehe:smallint;
raster_breite_max,raster_hoehe_max:smallint;
//allgemeine leben-funktionen
procedure leben_neu(
var ar:array of TLeben;
jaeger_ok:boolean
);
function leben_finde(
var ar:array of tleben;
inx:smallint;var x,y:smallint;
jaeger_ok:boolean
):boolean;
[...]
procedure dichte_ar_setzen(
x,y:smallint;
jaeger_ok,
clr_ok:boolean
);
function bremsen_pause_abbruch:boolean;
end;
Jeder Array-Eintrag in "jaeger_ar" bzws. "beute_ar"
steht für eine potenzielle Lebensform. I.d.R. werden aber nicht alle
Array-Einträge "aktiv" sein, d.h., sich auf dem Bildschirm herumtummeln.
Neben den Arrays für Jäger und Beute gibt es auch noch das
zwei-dimensionale Array "dichte_ar" vom Typ "Char".
Das wird zum einen zur Optimierung der Grafikausgabe verwendet,
zum anderen, um die Dichte der Population (entweder der Jäger
oder der Beute) schnell erfassen zu können.
Beispiel: Folgender Zusammenhang besteht zwischen
den Arrays "jaeger_ar", "beute_ar" und "dichte_ar".
Die Indizes des Dichte-Arrays entsprechen den x/y-Koordinaten der
Tiere geteilt durch deren Grösse. Sei die Grösse vom Benutzer auf
100 Pixel festgelegt, dann gilt:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
jaeger_ar[0]=(x= 0,y= 0,zaehler=10,gefressen=3);
jaeger_ar[1]=(x=500,y=500,zaehler=20,gefressen=0);
beute_ar[0] =(x=100,y=200,zaehler=0,gefressen=0);
beute_ar[1] =(x=200,y=200,zaehler=2,gefressen=0);
beute_ar[2] =(x=300,y=200,zaehler=4,gefressen=0);
beute_ar[3] =(x=100,y=300,zaehler=5,gefressen=0);
Daraus folgt das Dichte-Array:
0 1 2 3 4 5
0: j _ _ _ _ _ _ _ ... _ _
1: _ _ _ _ _ _ _ _ ... _ _
dichte_ar[x,y]=( 2: _ b b b _ _ _ _ ... _ _ )
3: _ b _ _ _ _ _ _ ... _ _
4: _ _ _ _ _ j _ _ ... _ _
...
_ _ _ _ _ _ _ _ ... _ _
- Der Jäger oben links benötigt nur noch eine Beute,
um sich selbst zu duplizieren (weil 'gefressen=3')
- Die Beute oben links wird sich in der nächsten
Evolutions-Runde verdoppeln (weil 'zaehler=0').
Visualisierung: Grafische Wiedergabe der obigen Arrays von Jäger und Beute.
Wie bei Delphi üblich, wird direkt nach dem Programmstart die
"FormCreate"-Prozedur aufgerufen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
//-------------------------------------------------------
//programm-start
//-------------------------------------------------------
procedure Thauptf.FormCreate(Sender: TObject);
var
r:smallint;
se:tspinedit;
begin
//echter zufall
randomize;
//umgebung setzen
caption:=_caption;
homedir:=extractfilepath(application.exename);
mitte_p.ParentBackground:=false;
mitte_p.color:=clblack;
mitte_p.Align:=alclient;
pb.Align:=alclient;
optionen_p.ParentBackground:=false;
optionen_p.color:=clsilver;
//anzahl jaeger option: minima und maxima
jaeger_start_se.MinValue:=1;
jaeger_start_se.MaxValue:=_leben_max;
jaeger_max_se.MinValue:=1;
jaeger_max_se.MaxValue:=_leben_max;
//anzahl beute option: minima und maxima
beute_start_se.MinValue:=1;
beute_start_se.Maxvalue:=_leben_max;
beute_max_se.MinValue:=1;
beute_max_se.Maxvalue:=_leben_max;
//puffer-bitmap initialisieren
bmp:=tbitmap.Create;
bmp.PixelFormat:=pf24bit;
bmp.canvas.Brush.style:=bssolid;
bmp.canvas.pen.width:=1;
bmp.Canvas.pen.color:=clblack;
//parameter einladen: spinedits fuellen
with tinifile.create(homedir+_inifn) do begin
for r:=0 to componentcount-1 do begin
if not(components[r] is tspinedit) then continue;
se:=tspinedit(components[r]);
se.value:=readinteger('param',se.name,se.value);
end;
free;
end;
knoepfe_setzen(false);
end;
Zu Beginn wird die PaintBox-Komponenten "pb" maximiert. Auf ihr
erfolgt später die die Evolution-Ausgabe. Um dabei hässliches Flackern zu
vermeiden, wird die Grafik zuvor in der Puffer-Bitmap "bmp"
verarbeitet, die hier ebenfalls initialisiert wird.
Die diversen Programm-Parameter werden alle in Komponenten
vom Typ "TSpinEdit" verwaltet. Um deren Werte aus der
INI-Datei "_inifn" (siehe "Konstanten") einzuladen, durchlaufen
wir sämtliche Komponenten der Form und filtern über den "is"-Operator
diejenigen vom Typ "TSpinEdit" heraus. Das erspart es uns, jeden
SpinEdit-Wert mit Namensangabe einzeln auslesen zu müssen.
Den gleichen Trick nutzen wir bei Programmende erneut, um
die SpinEdit-Werte in der INI-Datei abzuspeichern. Das geschieht in
der Prozedur "FormDestroy" folgendermassen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
//-------------------------------------------------------
//programm-ende
//-------------------------------------------------------
procedure Thauptf.FormDestroy(Sender: TObject);
var
r:smallint;
se:tspinedit;
begin
//parameter sichern
deletefile(homedir+_inifn);
with tinifile.create(homedir+_inifn) do begin
for r:=0 to componentcount-1 do begin
if not(components[r] is tspinedit) then continue;
se:=tspinedit(components[r]);
writeinteger('param',se.name,se.value);
end;
free;
end;
bmp.free;
end;
//-------------------------------------------------------
//programm-ende moeglich?
//-------------------------------------------------------
procedure Thauptf.FormCloseQuery(Sender:TObject;var CanClose:bool);
begin
//ist evolution fertig?
canclose:=(startb.caption='Start');
end;
//-------------------------------------------------------
//tastatur-ereignis abfangen
//-------------------------------------------------------
procedure Thauptf.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if key=vk_escape then close;
end;
Hier ein Beispiel für eine mögliche INI-Datei "pixelevolution.ini":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
[param]
jaeger_max_se=2050
beute_max_se=3000
groesse_se=100
jaeger_lebensdauer_se=100
beute_teildauer_se=12
bremse_se=10000
jaeger_start_se=1
beute_start_se=4
jaeger_lebensdauer_start_se=100
beute_teildauer_start_se=25
jaeger_lern_se=100
beute_lern_se=100
jaeger_variation_se=5
beute_variation_se=5
Um den Evolutions-Prozess anzustossen, wird vom Benutzer der
"Start"-Knopf betätigt. Dies bewirkt Folgendes:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
//-------------------------------------------------------
//start-knopf gedrueckt
//-------------------------------------------------------
procedure Thauptf.startbClick(Sender: TObject);
begin
if startb.caption='STOPP' then begin
//evolution stoppen
startb.caption:='Start';
exit;
end;
//evolution beginnen
evolution;
end;
Zunächst wird anhand der "caption" des Knopfes "startb" geprüft,
ob dort "STOPP" steht. Ist dies der Fall, läuft die Evolution
bereits. Die Beschriftung wird in diesem Fall auf "Start" zurückgesetzt,
und das Programm erkennt etwas später, dass die Evolution manuell abgebrochen
wurde.
Läuft dagegen der Evolution-Prozess noch nicht, so wird dieser nun über den Aufruf
von "evolution" gestartet.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
//-------------------------------------------------------
//evolution-haupt-schleife
//-------------------------------------------------------
procedure thauptf.evolution;
var
dichte,dichte_max:smallint;
runden_zaehler:smallint;
duplikat_ok:boolean;
begin
knoepfe_setzen(true);
//evolutions-parameter setzen
evolution_init;
//ewig-schleife beginnen
runden_zaehler:=0;
repeat
//evolutions-runde anzeigen
evolution_ausgabe;
//abbruchs-kriterien erfuellt?
if bremsen_pause_abbruch then break;
//jaeger auf beute zubewegen
jaeger_bewegen(dichte,dichte_max,duplikat_ok);
//lern-parameter adaptieren, abhaengig von
//fress-erfolg und dichte der jaeger
jaeger_lernen(dichte,dichte_max,duplikat_ok);
//beute vor jeaegern fliehen lassen
beute_bewegen(dichte,dichte_max,duplikat_ok);
//lern-parameter adaptieren, abhaengig von
//teilung-erfolg und dichte der beute
beute_lernen(dichte,dichte_max,duplikat_ok);
//rundenzaehler erhoehen und ausgeben
inc(runden_zaehler);
runden_zaehler_e.Text:=inttostr(runden_zaehler);
until false;
knoepfe_setzen(false);
end;
Die obige Prozedur ist das Kernstück von "Pixel-Evolution".
Es geschieht hier Folgendes:
-
Zuerst werden mit "knoepfe_setzen(true)" diverse Schalter
und SpinEdits aktiviert bzw. deaktiviert.
-
Es folgt die Prozedur "evolution_init", bei der alle
Umgebungsvariablen eingerichtete werden, die für den Ablauf
des Evolution-Prozesses wichtig sind.
-
Nun beginnt eine repeat-Schleife, die erst unterbrochen
wird, wenn die Evolution ein "natürliches" Ende findet, d.h.,
wenn entweder alle Jäger und/oder alle Beute-Tiere ausgestorben
sind. Alternativ kann die Schleife durch den Benutzer
unterbrochen werden, indem dieser den "STOPP"-Schalter betätigt.
-
Über die Prozedur "evolution_ausgabe" wird der aktuelle Bestand
von Jäger und Beute auf den Bildschirm gebracht.
-
Mittels der Funktion "bremsen_pause_abbruch" wird überprüft,
ob der Evolutions-Prozess beendet wurde. Ist dem so, dann wird
die Schleife verlassen.
-
Jetzt werden alle aktiven Jäger gesteuert, indem die Prozedur
"jaeger_bewegen" aufgerufen wird. Jedes Tier richtet
sich dabei jeweils nach der ihm am nahesten liegenden Beute.
Als Ergebnis erhält man die aktuelle Dichte der Population
der Jäger zurück ("dichte"), sowie die in der aktuellen
Situation maximal mögliche Population ("dichte_max").
Der Parameter "duplikat_ok" ist "true", sofern sich
mindestens ein Jäger hat duplizieren, sprich, fortpflanzen,
können.
-
Es folgt die Prozedur "jaeger_lernen", an welche die zwei
Dichte-Parameter und der mögliche Duplizierungs-Erfolg übergeben
werden. Ausgehend von diesen Daten lernen die Jäger für die
nächste Generation derart, dass ihre Lebensdauer modifiziert
wird.
-
Jetzt werden auch die (noch) aktiven Beute-Tiere in Bewegung gebracht.
Den Job erledigt die Prozedur "beute_bewegen". Jedes Tier
ist dabei so programmiert, dass es sich von dem ihm am nahesten
befindlichen Jäger fortbewegt. Als Ergebnis erhält man wie
bei den Jägern die aktuelle Dichte "dichte" und maximal
mögliche Dichte "dichte_max" der Population der Beute zurück.
Falls sich mindestens ein Beute-Tier hat replizieren können,
wird "duplikat_ok" auf "true" gesetzt.
-
Auch die Beute lernt aus den Dichte-Verhältnissen sowie dem
Duplizierungserfolg für die nächste Generation, indem diesesmal
deren Teildauer in der Prozedur "beute_lernen" modifiziert wird.
-
Zuletzt wird die inkrementierte Rundenzahl im SpinEdit
"runden_zaehler_e" ausgegeben. Danach beginnt die
Schleife von vorne.
-
Wurde die Schleife verlassen, ist der der Evolutions-Prozess am Ende.
Die Prozedur "knoepfe_setzen(false)" sorgt dafür, dass
verschiedene Form-Komponenten entsprechend (zurück)gesetzt werden.
Start und Ende: Zu Beginn startet die Evolution mit 5 Jägern
(rote Kreise) und 10 Beute-Tieren (grüne Quader) - und endet nach 77
Runden mit dem Sieg der Jäger.
Wie wir oben gesehen haben, wird in der Prozedur "evolution" als erstes
die Prozedur "knoepfe_setzen(true)" aufgerufen.
Dadurch werden verschiedene Schalter und SpinEdits des Haupt-Formulars
aktiviert bzw. deaktiviert, je nachdem, ob sie während des Laufs
änderbar sind oder nicht:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
//-------------------------------------------------------
//aktiviert/deaktiviert knoepfe und spinedits
//abhaengig, ob evolution laeuft oder nicht
//-------------------------------------------------------
procedure thauptf.knoepfe_setzen(evolution_ok:boolean);
begin
jaeger_start_se.enabled:=not evolution_ok;
jaeger_zaehler_e.enabled:=evolution_ok;
jaeger_lebensdauer_start_se.enabled:=not evolution_ok;
jaeger_lebensdauer_se.enabled:=evolution_ok;
beute_start_se.enabled:=not evolution_ok;
beute_zaehler_e.enabled:=evolution_ok;
beute_teildauer_start_se.Enabled:=not evolution_ok;
beute_teildauer_se.enabled:=evolution_ok;
if evolution_ok then begin
startb.caption:='STOPP';
pauseb.caption:='Pause';
end
else begin
startb.caption:='Start';
pauseb.caption:='Pause';
end;
pauseb.enabled:=evolution_ok;
groesse_se.enabled:=not evolution_ok;
end;
Das SpinEdits "jaeger_start_se" etwa beschreibt,
wie viele Jäger zu Beginn des Evolution-Prozesses generiert
werden sollen. Dieser Wert kann sinnvollerweise nur dann geändert
werden, sofern die Evolution noch nicht gestartet ist.
Die Anzahl der aktuell aktiven Jäger wird wiederum im Edit
"jaeger_zaehler_e" wiedergegeben. Diese Kompenente ist nur
während des Evolution-Prozesses aktiv.
Eine Besonderheit stellt der Schalter "startb" dar: Dessen
Attribut "caption" wechselt von "Start" auf "STOPP",
so wie die Evolution läuft. Das Programm kann anhand der Beschriftung
also jederzeit feststellen, ob die Evolution noch aktiv ist oder nicht.
Knöpfe I: Die Schalter und SpinEdits vor bzw. nach der Evolution.
Knöpfe II: Die Schalter und SpinEdits während der Evolution.
Nachdem in der Prozedur "evolution" die Knöpfe gesetzt wurden, wird
nun die Prozedur "evolution_init" aufgerufen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
//-------------------------------------------------------
//initiierung der evolutions-umgebung
//-------------------------------------------------------
procedure thauptf.evolution_init;
var
r:smallint;
begin
mitte_pResize(nil);
//plausibiliaet fuer startwerte
if jaeger_start_se.value>jaeger_max_se.value then
jaeger_start_se.value:=jaeger_max_se.value;
jaeger_lebensdauer_se.value:=jaeger_lebensdauer_start_se.value;
if beute_start_se.value>beute_max_se.value then
beute_start_se.value:=beute_max_se.value;
beute_teildauer_se.value:=beute_teildauer_start_se.value;
//jaeger- und beute-array loeschen
for r:=0 to _leben_max-1 do begin
Jaeger_ar[r].zaehler:=_leben_unbenutzt;
Beute_ar[r].zaehler:=_leben_unbenutzt;
end;
//neues leben schaffen
leben_neu(jaeger_ar,true);
leben_neu(beute_ar,false);
end;
Als erstes sorgt hier die Prozedur "mitte_pResize" dafür, dass alle
Variablen aktualisert werden, die die Grössenverhältnisse der Grafikausgabe
in irgendeiner Form berücksichtigen müssen.
Danach findet eine Plausibilitäts-Prüfung verschiedener
Programm-Parameter statt. Eventuell werden diese auch korrigiert.
So darf z.B. die Anzahl Jäger zu Beginn der Evolution natürlich
nicht die Anzahl maximal möglicher Jäger überschreiten.
Anschliessend werden in einer Schleife die Arrays von Jäger und
Beute "gelöscht", indem den darin enthaltenen einzelnen
"TLeben"-Instanzen ein "zaehler" kleiner Null
vermittelt wird.
Durch zwei-maliges Aufrufen von "lebe_neu" mit jeweils
verschiedenen Parametern wird zuletzt die vom Benutzer vorgegebene
Start-Anzahl an Jägern und Beute-Tieren in zufälliger Weise generiert.
Der Aufruf von "mitte_pResize(nil)", den wir eben in
der Prozedur "evolution_init" gesehen haben, bewirkt das
gleiche wie die manuelle Änderung der Grösse der Haupt-Form mit
der Maus durch den Benutzer: Die Parameter für die Grafikausgabe
werden an die neuen Dimensionen angepasst.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
//-------------------------------------------------------
//groessenaenderung-ereignis abfangen
//-------------------------------------------------------
procedure Thauptf.mitte_pResize(Sender: TObject);
begin
//puffer-bitmap anpassen
bmp.width:=pb.width;
bmp.height:=pb.height;
//block-parameter anpassen (z.B. fuer dichte-koordinaten)
raster_rand_breite:=(pb.width mod groesse_se.value) div 2;
raster_rand_hoehe:=(pb.height mod groesse_se.value) div 2;
raster_breite_max:=(pb.width div groesse_se.value)-1;
raster_hoehe_max:=(pb.height div groesse_se.value)-1;
end;
Beispiel: Angenommen unser Spielfeld (TPaintBox "pb") hat die Dimensionen
1006 x 6008 Pixel. Die Grösse von Jäger und Beute "groesse_se"
wurde vom Benutzer zuvor auf 100 Pixel festgelegt. Dann gilt:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
Anzahl Leben pro Zeile (raster_breite_max): (1006 div 100) -1 = 9
Anzahl Leben pro Spalte (raster_hoehe_max ): ( 608 div 100) -1 = 5
Um die Ausgabe zu zentrieren muss der überschüssige Rand
oben und unten berechnet werden:
raster_rand_breite = (1006 mod 100) div 2 = 6 div 2 = 3 Pixel
raster_rand_hoehe = (1008 mod 100) div 2 = 8 div 2 = 4 Pixel
TPaintBox 'pb' baut sich also so auf:
1006 Pixel breit x 608 Pixel hoch
********************************
* 4 Pixel *
* ----------------------- *
* 3 | 0 1 2 3 4 5 6 7 8 9 | 3 *
* | 1 | *
* p | 2 | P *
* i | 3 | i *
* x | 4 | x *
* e | 5 | e *
* l ----------------------- l *
* 4 Pixel *
********************************
Grössen-Anpassung: Links verfügt das Leben über mehr Raum nach oben
und unten, rechts hingegen nach links und rechts. Die Anzahl möglicher Tiere
je Spalte und Zeile wird in den Parametern "raster_rand_breite" und
"raster_rand_hoehe" festgehalten.
Nach der eben beschriebenen Grössen-Adaption folgt in "evolution_init"
die Löschung der Jäger- und Beute-Arrays. Anschliessend werden diese mittels
Aufruf von "leben_neu" gleich wieder mit zufällig platzierten, neuen
Tieren gefüllt.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
//--------------------------------------------------------
//fuelle jaeger-array oder beute-array mit zufaellig
//verteiltem leben
//--------------------------------------------------------
procedure thauptf.leben_neu(
var ar:array of TLeben;
jaeger_ok:boolean
);
var
r,x,y,dum,max:smallint;
begin
//zu generierende anzahl jaeger bzw. beute
if jaeger_ok then max:=jaeger_start_se.value
else max:=beute_start_se.value;
dum:=_leben_frei;
for r:=0 to max-1 do begin
//suche freie position
repeat
//zufaellige koordinaten
x:=pos_raster_x(random(pb.width));
y:=pos_raster_y(random(pb.height));
//pruefe, ob dort kein leben ist
until not beute_vorhanden(x,y,dum) and
not jaeger_vorhanden(x,y,dum);
//initiiere neuen jaeger bzw. neue beute
if jaeger_ok then
//lebensdauer mit variations-anteil
ar[r].zaehler:=jaeger_variation
else
//teildauer mit variations-anteil
ar[r].zaehler:=beute_variation;
ar[r].x:=x;
ar[r].y:=y;
ar[r].gefressen:=0;
end;
end;
Anhand des übergebenen Parameters "jaeger_ok" weiss das Programm,
ob neue Jäger oder neue Beute-Tiere generiert werden sollen.
Das zugehörige "TLeben"-Array wird durchlaufen und
mit der vom Benutzer vorgegebenen Anzahl an Start-Tieren gefüllt
(dieser Wert steht in in den SpinEdits "jaeger_start_se" bzw.
"beute_start_se").
Dazu wird eine zufällige Position innerhalb des Lebensraums
ausgewählt, der durch die Dimensionen der TPaintBox "pb"
vorgegeben ist. Die Koordinaten werden dabei gerastert, sodass
sich die Tiere zwangsweise auf einer Art Gitter-Netz bewegen.
Durch Prüfung mittels der Funktionen "jaeger_vorhanden" und
"beute_vorhanden" wird sicher gestellt, dass die gefundenen
Koordinaten noch nicht von einem anderen Tier belegt sind. Da es in
unserem Modell keine Höhen-Dimension gibt, kann an einem Ort nämlich
immer nur eine Lebensform existieren.
Zuletzt wird dem neuen Jäger im "zaehler" etwas "Lebensdauer"
verpasst bzw. dem neuen Beute-Tier eine gewisse "Teildauer".
Die Werte dafür lieferen uns die Funktionen "jaeger_variation"
und "beute_variation". Wie deren Namen schon andeutet,
bekommen die neuen Tiere hierbei nicht alle exakt die gleiche
"Lebenskraft" verpasst.
Wie im echten Leben sind die Voraussetzungen für das Überleben in
der "Wildnis" von "Pixel-Evolution" also nicht von Geburt an
gleichverteilt.
Wie wir später noch sehen werden (Kapitel "Bewegung im Raster-Gitter"), sind die
Positionen von Jäger und Beute "normiert" und auf diese Weise in ein Raster gewungen.
Die Rasterung hat exakt die gleiche Dimensionen wie das weiter
oben vorgestellte Dichte-Array "dichte_ar". Das ist natürlich kein
Zufall. Denn dies ermöglicht es uns, relativ einfach festzustellen,
ob eine bestimmte Raster-Position bereits von einem Tier belegt ist
oder nicht.
Schauen wir uns dazu die Funktion "leben_vorhanden" an:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
//-------------------------------------------------------
//existiert jaeger oder beute auf koordinate?
//-------------------------------------------------------
function thauptf.leben_vorhanden(
var ar:array of TLeben;
x,y:smallint;
var inx:smallint;
jaeger_ok:boolean
):boolean;
var
r,xx,yy:smallint;
begin
//pixel-koordinaten in dichte-indizes umwandeln
xx:=x div groesse_se.Value;
yy:=y div groesse_se.Value;
//jaeger bzw. beute gefunden?
if jaeger_ok then result:=(dichte_ar[xx,yy]=_dichte_jaeger)
else result:=(dichte_ar[xx,yy]=_dichte_beute);
//genauer ort egal?
if inx=-1 then exit;
//kein leben gefunden?
if not result then exit;
//bestimme index des gefundenen lebens
result:=false;
for r:=0 to _leben_max-1 do begin
//ignoriere inaktive leben
if ar[r].zaehler=_leben_unbenutzt then exit;
if ar[r].zaehler=_leben_frei then continue;
//leben mit gefunden leben aus dichte-array identisch?
if (ar[r].x<>x)or(ar[r].y<>y)then continue;
//yep, liefere index zurueck
inx:=r;
result:=true;
break;
end;
end;
Wir teilen zunächst die übergebenen Koordinatenwerte "x" und "y"
der zu prüfenden Position durch die Grösse von Jäger und Beute
"groesse_se.Value". Dadurch konvertieren wir die Pixel-Koordinaten
in die entsprechenden Index-Werte des Dichte-Arrays "dichte_ar".
Jetzt prüfen wir weiter, ob sich im Dichte-Array bei den berechneten
Indizes ein Zeichen für Jäger oder Beute befindet - oder aber ein Leerzeichen.
In letzterem Fall können wir die Prozedur mit dem Ergebnis "false"
verlassen; auf dieser Position befindet sich kein Lebewesen.
Ansonsten wird geprüft, ob der übergebene Parameter "inx" den Wert
"-1" hat. Ist dem so, dann interessiert uns nicht weiter, welches
konkrete Lebewesen sich dort aufhält. Wir verlassen die Prozedur mit
"true"; auf dieser Position befindet sich ein Lebewesen.
Ansonsten durchlaufen wir zusätzlich das übergebene "TLeben"-Array
und vergleichen die Koordinaten der darin enthaltenen Lebewesen mit den
zu prüfenden "x"- und "y"-Werten. Da in beiden Fällen
gerastert wurden, stimmen sie bei genau einem Tier exakt überein. Ist
dieses gefunden, wird sein Array-Index in "inx" gerettet und
die Prozedur mit "true" verlassen; auf dieser Position befindet
sich ein Lebewesen - und wir wissen zudem auch, welches genau.
Weil der Sinn so im Source leichter zu erfassen ist, wird die
"leben_vorhanden"-Funktion von zwei weiteren Funktionen
mit den passenden Parametern aufgerufen und das Ergebnis einfach
durchgeschleift:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
//-------------------------------------------------------
//ist jaeger auf koordinate x/y vorhanden?
//-------------------------------------------------------
function thauptf.jaeger_vorhanden(x,y:smallint;var inx:smallint):boolean;
begin
result:=leben_vorhanden(jaeger_ar,x,y,inx,true);
end;
//-------------------------------------------------------
//ist beute auf koordinate x/y vorhanden?
//-------------------------------------------------------
function thauptf.beute_vorhanden(x,y:smallint;var inx:smallint):boolean;
begin
result:=leben_vorhanden(beute_ar,x,y,inx,false);
end;
Zurück in die Prozedur "leben_neu". Wir nehmen an, es wurde erfolgreich
eine zufällige Position ermittelt, auf der sich weder ein Jäger noch eine Beute
befindet.
Auf diese Koordinaten wird nun das neues Leben gesetzt, indem im
übergebenen "TLeben"-Array der aktuelle "r"-Index mit
Werten gefüllt wird.
Die gerasterte, freie x-/y-Position haben wir ja gerade ermittelt - sie wird
entsprechend beim neuen Leben eingetragen. Das Attribut "zaehler"
bekommt - je nachdem, ob ein Jäger oder eine Beute generiert werden soll -,
über die Funktion "jaeger_variation" bzw. "beute_variation"
einen neuen Wert grösser Null zugewiesen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
//-------------------------------------------------------
//neuer lebensdauer-wert eines jaeger
//variiert um zufallswert nach oben und unten
//-------------------------------------------------------
function thauptf.jaeger_variation:smallint;
begin
result:=jaeger_variation_se.value;
result:=
jaeger_lebensdauer_se.value+
random(result)-random(result);
if result<_jaeger_lebensdauer_min then
result:=_jaeger_lebensdauer_min;
end;
//-------------------------------------------------------
//neuer teildauer-wert einer beute
//variiert um zufallswert nach oben und unten
//-------------------------------------------------------
function thauptf.beute_variation:smallint;
begin
result:=beute_variation_se.value;
result:=
beute_teildauer_se.value+
random(result)-random(result);
if result<_beute_teildauer_min then
result:=_beute_teildauer_min;
end;
Wir sehen: Dem frischen Jäger in "jaeger_ar" wird seine Lebensdauer
aus dem SpinEdit "jaeger_lebensdauer_se" übertragen. Es handelt
sich dabei um einen während der Evolution gelernten Wert, wie wir
später noch sehen werden. Er ändert sich ständig. Zu Beginn ist er
jedoch auf den Wert vom SpinEdit "jaeger_lebensdauer_start_se"
gesetzt worden.
Damit nicht alle Jäger der selben Generation über die exakt gleiche
Lebensdauer verfügen, wird diese zusätzlich über das SpinEdit
"jaeger_variation_se" nach unten oder oben zufällig modifiziert.
Evolutionär gesehen handelt es sich hierbei quasi um eine Mutation.
Das sorgt für eine gewisse Streuung in den Eigenschaften der
Population. So finden sich unter Umständen z.B. auch dann langlebigen
Jäger, wenn die aktuelle Jäger-Dichte im Allgemeinen eher eine kurzen
Lebensdauer propagiert.
In ähnlicher Weise wird die Teildauer der Beute variiert.
Variation der Lebenskraft: Die Jäger drohen zu dominieren. Ihre
dunkle Färbung zeigt aber an, dass sie nur noch über wenig Lebenskraft
verfügen. Die Beute hingegen ist sehr hell; sie steht kurz vor der
Replikation. Einer der Jäger ist allerdings auch hell - er ist eine
zufällige Mutation mit längerer Lebensdauer.
Wir befinden uns wieder in der Prozedur "evolution".
Gerade wurde über "evolution_init" die Umgebung initialisiert
und mittels "leben_neu" die vom Benutzer vorgegeben
Startmenge von Jägern und Beute geschaffen. Dann startete die
Ewig-Schleife. Der eigentliche Evolutionsprozess läuft.
Es wurde bereits mehrfach erwähnt, dass sich die einzelnen Lebensformen
in "Pixel-Evolution" auf einer Art Raster-Gitter bewegen müssen.
Auch die eben in "leben_neu" generierten "TLeben"-Instanzen
richten sich danach.
Doch was bedeutet das konkret?
Jedes imaginäre Raster-Rechteck entspricht in seinen Abmessungen exakt denen
der "TLeben". Jedes Tier kann weiterhin nur ganz oder gar nicht auf einem
Raster-Rechteck Position beziehen; ein "Zwischen zwei Raster-Rechtecken"
gibt es in der Welt von "Pixel-Evolution" nicht.
Rasterung: Links überschneiden sich zwei Beute-Tiere. Dies ist
in "Pixel-Evolution" nicht erlaubt. Hier dürfen alle Lebensform nur
innerhalb imaginärer Raster-Linien nebeneinander (oder übereinander)
existieren - so wie im Beispiel rechts.
Und wie erreichen wir diese Orientierung am Raster-Gitter?
Die x- und y-Werte der "TLeben" werden bei jeder Änderung durch die Funktionen
"pos_raster_x" bzw. "pos_raster_y" normiert, sodass die Koordinaten
stets exakt auf den Rändern des imaginären Rasters liegen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
//-------------------------------------------------------
//skaliert x-koordinaten-werte derart, dass sie sich
//auf block-raster-spalten bewegen.
//-------------------------------------------------------
function thauptf.pos_raster_x(x:smallint):smallint;
begin
x:=x div groesse_se.Value;
//verbotene koordinaten abfangen
if x<0 then x:=0;
if x>raster_breite_max then x:=raster_breite_max;
//um randwert korrigieren
result:=raster_rand_breite+(x*groesse_se.Value);
end;
//-------------------------------------------------------
//skaliert y-koordinaten-werte derart, dass sie sich
//auf block-raster-zeilen bewegen.
//-------------------------------------------------------
function thauptf.pos_raster_y(y:smallint):smallint;
begin
y:=y div groesse_se.Value;
//verbotene koordinaten abfangen
if y<0 then y:=0;
if y>raster_hoehe_max then y:=raster_hoehe_max;
//um randwert korrigieren
result:=raster_rand_hoehe+(y*groesse_se.Value);
end;
Beispiel: Verhinderung von Überschneidungen durch Rasterung:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
Es sei: Grösse von 'TLeben' = groesse_se.Value = 100 Pixel
(1) Ein Jäger hockt auf den Koordinaten x=60 und y=120 herum
=> x gerastert ist dann:
x = x div groesse_se.Value = 60 div 100 = 0
x = raster_rand_breite+(x*groesse_se.Value) = 3 + (0*100) = 3
=> und y gerastert ergibt:
y = y div groesse_se.Value = 120 div 100 = 1
y = raster_rand_hoehe+(x*groesse_se.Value) = 4 + (1*100) = 104
(2) Ein zweiter Jäger versucht sich nun auf die von (1) abweichenden
Koordinaten x=90 und y=100 zu platzieren
=> x gerastert ergibt jedoch:
x = x div groesse_se.Value = 90 div 100 = 0
x = raster_rand_breite+(x*groesse_se.Value) = 3 + (0*100) = 3
=> und y gerastert ergibt:
y = y div groesse_se.Value = 100 div 100 = 1
y = raster_rand_hoehe+(x*groesse_se.Value) = 4 + (1*100) = 104
==> (1) und (2) würden den gleichen Ort einnehmen, was nicht erlaubt
ist. (2) muss sich daher neue Koordinaten suchen, wenn er den
Standort wechseln will.
Bei jeden Evolutions-Schleifen-Durchgang werden alle aktiven Jäger
und alle aktiven Beute-Tiere auf dem Bildschirm ausgegeben.
Dazu verwenden wir die Prozedur "evolution_ausgabe":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
//-------------------------------------------------------
//gib aktuelle evolutions-runde aus
//-------------------------------------------------------
procedure thauptf.evolution_ausgabe;
var
x,y,zaehler:smallint;
begin
//puffer-bitmap loeschen
bmp.canvas.brush.color:=clblack;
rectangle(bmp.Canvas.Handle,0,0,bmp.width,bmp.Height);
//dichte-array leeren
for y:=0 to raster_hoehe_max do begin
for x:=0 to raster_breite_max do begin
dichte_ar[x,y]:=_dichte_frei;
end;
end;
//zaehle jaeger und zeichne sie auf puffer-bitmap
zaehler:=leben_zaehlen_malen(jaeger_ar,true);
jaeger_zaehler_e.Text:=inttostr(zaehler);
jaeger_zaehler:=zaehler;
//kein jaeger mehr gefunden? dann evolution fertig
if zaehler=0 then startb.caption:='Start';
//zaehle beute und zeichne sie auf puffer-bitmap
zaehler:=leben_zaehlen_malen(beute_ar,false);
beute_zaehler_e.Text:=inttostr(zaehler);
beute_zaehler:=zaehler;
//keine beute mehr gefunden? dann evolution fertig
if zaehler=0 then startb.caption:='Start';
//puffer-bitmap auf PaintBox kopieren
bitblt(
pb.canvas.Handle,0,0,pb.width,pb.height,
bmp.Canvas.Handle,0,0,
srccopy
);
end;
Hier wird als erstes die Puffer-Bitmap "bmp" mit einem schwarzen
Rechteck übermalt, d.h., gelöscht.
Anschliessend wird das Dichte-Array "dichte_ar" mit Leerzeichen
(="dichte_frei", siehe "Konstanten") gefüllt, also ebenfalls gelöscht.
Über den nun folgenden Aufruf der Funktion "leben_zaehlen_malen" werden
alle aktiven Jäger gezählt und in die Puffer-Bitmap eingezeichnet.
Die gleiche Prozedur wird im Anschluss daran auch auf die Beute angewendet.
Zuletzt wird über den API-Befehl "bitblt" die frisch gefüllte
Puffer-Bitmap "bmp" auf den Canvas von PaintBox "pb"
kopiert - und damit zur Anzeige gebracht.
Visualisierung des Modells: Jäger und Beute kurz nach
Evolutionsbeginn. Alle Tiere sind zufällig positioniert worden. Jetzt
beginnt das grosse Fressen.
Wie wir gerade gesehen haben, werden in der Prozedur "evolution_ausgabe"
alle aktiven Jäger in die Puffer-Bitmap "bmp" gezeichnet sowie ihre
Anzahl ermittelt. Im Anschluss folgte das Gleiche mit der Beute. Dabei kam
die Funktion "leben_zaehlen_malen" zum Einsatz, die wir uns hier
näher anschauen wollen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
//-------------------------------------------------------
//alle aktiven jaeger bzw. beute werden gezaehlt und
//gemaess ihrer koordinaten in puffer-bitmap eingezeichnet
//-------------------------------------------------------
function thauptf.leben_zaehlen_malen(
var ar:array of tleben; //jaeger- oder beute-array
jaeger_ok:boolean //werden jaeger betrcahtet?
):smallint;
const
_farbe_max=180;
var
sz,r,xx,yy,schritte_max:smallint;
farbe_delta:double;
rot_aufheller,farb_anteil:byte;
farbe:tcolor;
begin
result:=0;
sz:=groesse_se.value;
//maximal moegliche lebens-schritte
if jaeger_ok then
schritte_max:=
jaeger_lebensdauer_se.maxvalue+
jaeger_variation_se.maxvalue
else
schritte_max:=
beute_teildauer_se.maxvalue+
beute_variation_se.maxvalue;
//farbaenderung je lebensschritt
farbe_delta:=(_farbe_max/schritte_max);
rot_aufheller:=(255-_farbe_max);
//durchlaufe jaeger-/beute-array
for r:=0 to _leben_max-1 do begin
//ignoriere inaktive lebewesen
if ar[r].zaehler=_leben_unbenutzt then exit;
if ar[r].zaehler=_leben_frei then continue;
//aktiven jaeger/beuite gefunden
//reduziere lebens- bzw. teildauer um ein jahr
ar[r].zaehler:=ar[r].zaehler-1;
//jaeger/beute immer noch im rennen?
if ar[r].zaehler=_leben_frei then continue;
//treffer erhoehen
result:=result+1;
//lebewesen in dichte-array eintragen
xx:=ar[r].x div sz;
yy:=ar[r].y div sz;
if jaeger_ok then dichte_ar[xx,yy]:=_dichte_jaeger
else dichte_ar[xx,yy]:=_dichte_beute;
//rot/gruen/blau anhand von 'lebenskraft' berechnen
farb_anteil:=trunc(ar[r].zaehler*farbe_delta);
//und jaeger/beute in puffer-bitmap eintragen
if jaeger_ok then begin
//je kleiner lebensdauer, desto dunkler
farbe:=rgb(
farb_anteil+rot_aufheller,
farb_anteil,
farb_anteil
);
bmp.Canvas.brush.color:=farbe;
ellipse(
bmp.canvas.handle,
ar[r].x, ar[r].y,
ar[r].x+sz,ar[r].y+sz
);
end
else begin
//je laenger teildauer, desto dunkler
farb_anteil:=255-farb_anteil;
farbe:=rgb(farb_anteil,255,farb_anteil);
bmp.Canvas.brush.color:=farbe;
rectangle(
bmp.canvas.handle,
ar[r].x, ar[r].y,
ar[r].x+sz,ar[r].y+sz
);
end;
end;
end;
In "schritte_max" wird festgehalten, wie lange die Lebensdauer
eines Jäger bzw. die Teildauer einer Beute maximal sein kann.
Die Zahl setzt sich zusammen aus der genetisch vorgegeben maximalen
Lebensdauer/Teildauer einer jeden Generation plus der maximalen
Variation noch oben hin (siehe Kapitel "Gelernte Variation").
Mithilfe dieser Information können wir ein "farbe_delta" berechnen.
Multipliziert mit dem aktuellen "zaehler" von Jäger oder Beute
berechnet sich ein Farbwert, der optisch wiedergibt, wie es mit der
Lebenskraft des Tieres aussieht: Je dunkler, desto kürzer
die verbleibende Lebenspanne (beim Jäger), je heller, desto näher
eine anstehende Teilung (bei der Beute).
Das übergebene TLeben-Array wird durchlaufen. Ein aktives Leben wird
daran erkannt, dass sein "zaehler"-Attribut grösser-gleich Null
ist. Inaktives Leben wird ignoriert.
In "result" wird die Anzahl aktiver Leben aufsummiert. Gleichzeitig
wird bei jedem Leben der "zaehler" dekrementiert, denn bei jedem
Aufruf von "leben_zaehlen_malen" ist quasi ein weiteres Jahr im
Evolutions-Prozess vergangen.
Anhand der Position des aktuellen Lebens können die Indizes des
Dichte-Arrays "dichte_ar" berechnet werden. Dort wird die
gefundene Lebensform entsprechend eingetragen.
Je nachdem, ob ein Jäger oder eine Beute vorliegt, wird zuletzt
mit der berechneten Farbabstufung ein roter Kreis für einen Jäger
oder ein grünes Rechteck für Beute in die Puffer-Bitmap eingezeichnet.
=
Jäger/Fuchs: Unser stolzer Jäger, einmal als Symbol, einmal in Natura
=
Beute/Hase: Unsere flinke und reproduktionsfreudige Beute im Spiel
Wir sind zurück in der Prozedur "evolution". Nach Ausgabe
von Jäger und Beute auf dem Bildschirm und Prüfung, ob der
Evolutionsprozess fortgesetzt werden kann, wird jetzt die Funktion
"jaeger_bewegen" aufgerufen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
//--------------------------------------------------------
//setze im dichte_array jaeger-, beute- oder frei-zeichen
//--------------------------------------------------------
procedure thauptf.dichte_ar_setzen(
x,y:smallint;
jaeger_ok:boolean;
clr_ok:boolean
);
begin
//leben-pixel-koordinaten umwandeln
//in dichte-array-koordinaten
x:=x div groesse_se.value;
y:=y div groesse_se.value;
//dort dichte-array fuellen
if clr_ok then dichte_ar[x,y]:=_dichte_frei
else if jaeger_ok then dichte_ar[x,y]:=_dichte_jaeger
else dichte_ar[x,y]:=_dichte_beute;
end;
//-------------------------------------------------------
//bewege die population der jaeger
//-------------------------------------------------------
procedure thauptf.jaeger_bewegen(
var dichte:smallint; //reale dichte um jaeger herum
var dichte_max:smallint; //maximal moegeliche dichte
var duplikat_ok:boolean //mindestens eine reproduktion?
);
var
r,inx,start_index:smallint;
x_gefunden,y_gefunden:smallint;
begin
start_index:=0;
duplikat_ok:=false;
dichte:=0;dichte_max:=0;
//durchlaufe jaeger-array
for r:=0 to jaeger_max_se.value-1 do begin
//ignoriere inaktive jaeger
if jaeger_ar[r].zaehler=_leben_unbenutzt then exit;
if jaeger_ar[r].zaehler=_leben_frei then continue;
//summiere reale und maximal moegliche dichte auf
dichte:=
dichte+
leben_dichte(jaeger_ar,r,true,dichte_max);
//suche naheste beute zum aktiven jaeger
if not leben_finde(beute_ar,r,x_gefunden,y_gefunden,false)then
continue;
//beute gefunden: bewege jaeger dorthin
inx:=leben_bewegung(jaeger_ar,r,x_gefunden,y_gefunden,true);
//hat jaeger beute erwischt?
if inx=_leben_frei then continue;
//jep: lass beute sterben
beute_ar[inx].zaehler:=_leben_frei;
dichte_ar_setzen(beute_ar[inx].x,beute_ar[inx].y,false,true);
//erhoehe beutetreffer des jaegers
jaeger_ar[r].gefressen:=jaeger_ar[r].gefressen+1;
//genug beute gerissen fuer reproduktion?
if jaeger_ar[r].gefressen<_beute_fuer_duplikat then
continue;
//ja, jaeger dupliziert sich (falls moeglich)
if leben_duplikat(jaeger_ar,r,true,start_index) then
duplikat_ok:=true;
end;
end;
Hier wird das Jäger-Array "jaeger_ar" durchlaufen und die aktiven
Tiere, also die, mit "zaehler" grösser Null, herausgefiltert.
Danach wird mittels der Funktion "lebe_dichte" die Population
direkt um den aktiven Jäger herum ermittelt. Das schauen wir uns gleich
noch genauer an.
Anschliessend suchen wir mit "leben_finde" diejenige Beute,
die relativ zum aktiven Jäger am wenigsten weit weg positioniert ist.
Ihre Koordinaten stehen in "x_gefunden" und "y_gefunden".
Die Funktion "leben_bewegung" lässt nun unseren Jäger in
Richtung der gefundenen Beute bewegen - zumindest mit einer
gewissen Wahrscheinlichkeit. Manchmal bleibt das Tier nämlich auch
einfach, wo es gerade ist. Falls der Jäger dabei eine Beute erreichen
konnte, steht deren Array-Index in der zurückgelieferten Variable
"inx".
Der Erfolg des Jägers ist der Verlust der Beute. Das gefressene
Tier mit Index "inx" wird gelöscht, indem sein Attribut
"zaehler" auf "_leben_frei" (=-1) gesetzt wird.
Gleichzeitig wird es unter Verwendung der Prozedur
"dichte_ar_setzen" auch aus dem Dichte-Array "dichte_ar"
eliminiert. Das ist wichtig, da es sonst - virtuelle - von einem
zweiten Jäger noch einmal gefressn werden könnte :-)
Und unser gerade gesättigter Jäger? Dessen Attribut "gefresssen"
wird jetzt natürlich erhöht. Und wenn er genügend Beute geschlagen
hat, kann er sich sogar über die Funktion "leben_duplikat"
unter Umständen selbst reproduzieren.
Anschliessend wird die Schleife erneut durchlaufen und der nächste
aktive Jäger bewegt, bis schliesslich alle Tiere durch sind.
Jäger-Bewegung I: Die Beute am unteren Rand
fürchtet sich vor der Übermacht der anrückenden Jäger
Jäger-Bewegung II: Die Jäger haben die Beute fast erreicht.
Sie selbst hat sich nur wenige Schritte nach rechts bewegen können,
die Flucht nach oben ist ihr versperrt.
Gerade haben wir gesehen, wie in der Prozedur "jaeger_bewegen"
die die Funktion "leben_dichte" zum Einsatz kam.
Hier wird berechnet, wie viele direkte Jäger-Nachbarn der aktuell
betrachtete Jäger mit dem übergebenen Array-Index "inx" besitzt.
Darüber hinaus wird im Var-Parameter "dichte_max"
festgehalten, wie viel direkte Nachbarn der Jäger maximal haben kann.
Dies sind i.d.R. acht Tiere, können aber auch weniger sein, etwa dann,
wenn sich der Jäger am Rand oder in einer Ecke des Modells befindet.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
//-------------------------------------------------------
//berechne dichte um ein lebenwesen
//bei jaegern gelten nur jaeger, bei beute nur beute
//liefert aktuelle dichte und maximal moegliche dichte zurueck
//maximal moegliche dichte kann variieren, wenn sich leben
//an raendern oder eckpunkten befindet
//
// Legende: j=jäger, b=beute
//
// Situation #1 Situation #2 Situation #3
//
// |jjj |j_j |_j_
// |jJj => dichte=8 |jJ_ => dichte=5 |J__ => dichte=3
// |jjj max=8 |jj_ max=8 |jj_ max=5
//
//-------------------------------------------------------
function thauptf.leben_dichte(
var ar:array of TLeben; //jaeger- oder leben-array
inx:smallint; //index des aktiven leben
jaeger_ok:boolean; //jaeger betrachten?
var dichte_max:smallint //maximal moegeliche dichte
):smallint;
var
ch:char;
//prufe im dichte-array auf jaeger/beute
function f(x,y:smallint):byte;
begin
result:=0;
//ueber rand oder eckpunkt heraus?
if (x<0)or(x>raster_breite_max) then exit;
if (y<0)or(y>raster_hoehe_max)then exit;
//artgenossen gefunden?
if dichte_ar[x,y]=ch then result:=1;
dichte_max:=dichte_max+1;
end;
var
x,y:smallint;
begin
if jaeger_ok then ch:=_dichte_jaeger
else ch:=_dichte_beute;
//pixel-koordinaten in array-idzes umrechnen
x:=ar[inx].x div groesse_se.Value;
y:=ar[inx].y div groesse_se.Value;
//zaehle artgenossen um index-leben herum
result:=
f(x-1,y-1)+f(x,y-1)+f(x+1,y-1)+
f(x-1,y )+ +f(x+1,y )+
f(x-1,y+1)+f(x,y+1)+f(x+1,y+1);
end;
Zunächst wird im Charakter "ch" festgehalten, nach
welchen Zeichen wir im Dichte-Array Ausschau halten müssen:
Im Falle eines Jägers ist das "_dichte_jaeger" (="j"),
im Falle von Beute ist das ""_dichte_beute" (="b");
Wir rechnen auf bereits vertraute Weise die gerasterten
x/y-Koordinaten des Tieres in die Indizes des Dichte-Array
"dichte_ar" um.
Über die interne Funktion "f" lassen wir uns "1"
oder "0" zurückliefern, jenachdem, ob auf der geprüften
Position im Dichte-Array das Zeichen "ch" gefunden wird
oder nicht.
Der Var-Parameter "dichte_max" wird im Gegensatz dazu
jedes Mal inkrementiert - sofern sich an der Prüf-Stelle theoretisch
eines der gesuchten Tiere befinden könnte.
Jäger-Dichte niedrig: Die Dichte der Population der Jäger ist relativ niedrig
Jäger-Dichte hoch: Die Dichte der Population der Jäger ist ausgesprochen hoch
Gerade wurde in der Prozedur "jaeger_bewegen" über die
Funktion "leben_dichte" ermittelt, wie eng beieinander
die Population der Jäger um den aktuellen Jäger ist.
Im nächsten Schritt gilt es, dass zum Jäger naheste Beute-Tier
zu ermitteln. Dazu verwenden wir die Funktion "leben_finde".
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
//-------------------------------------------------------
//finde nahestes leben zu index-tier
//
// 0 1 2 3 4
// 0 _ _ _ _ _ abstand jaeger zur beute =
// 1 _ j _ _ _ => abs(3-1)+abs(2-1) =
// 2 _ _ _ b _ 2+1 =
// 3 _ _ _ _ _ 3
//
//-------------------------------------------------------
function thauptf.leben_finde(
var ar:array of tleben; //jaeger- oder beute-array
inx:smallint; //index aktives leben
var x,y:smallint; //treffer-koordinaten
jaeger_ok:boolean //wird jaeger betrcahtet?
):boolean;
var
r,abstand,abstand_min:smallint;
begin
result:=false;
abstand_min:=-1;
x:=-1;y:=-1;
//durchlaufe leben-array
for r:=0 to _leben_max-1 do begin
//ignoriere inaktives leben
if ar[r].zaehler=_leben_unbenutzt then exit;
if ar[r].zaehler=_leben_frei then continue;
if jaeger_ok then
//abstand aktiver jaeger zu index-beute
abstand:=
abs(beute_ar[inx].x-jaeger_ar[r].x)+
abs(beute_ar[inx].y-jaeger_ar[r].y)
else
//abstand aktive beute zu index-jaeger
abstand:=
abs(jaeger_ar[inx].x-beute_ar[r].x)+
abs(jaeger_ar[inx].y-beute_ar[r].y);
//neues minimum gefunden?
if(abstand_min=-1)or(abstand<Abstand_min) then begin
//je: position und index merken
x:=ar[r].x;
y:=ar[r].y;
//neuen minmal-abstand merken
abstand_min:=abstand;
result:=true;
//direkter nachbar? naeher gehts nicht
if abstand_min<=groesse_se.Value then exit;
end;
end;
end;
Dazu durchlaufen wir das Beute-Array, welches als Parameter
"ar" übergeben wurde, und berechnen den Abstand einer
jeden Beute zum aktuellen Jäger. Der kleineste dabei ermittelte
Abstand wird in "abstand_min" gerettet, die Position des
zugehörigen Beute-Tiers in den Var-Parametern "x" und
"y" festgehalten.
Wird übrigens ein Abstand von "1" ermittelt, befindet sich die
aktuell betrachtete Beute irgendwo unmittelbar um den Jäger herum.
In diesem Fall kann der Suchprozess sofort beendet werden, denn noch
näher kann Beute dem Jäger ja nicht kommen.
Zurück in die Prozedur "jaeger_bewegen". Wir haben gerade
einen aktiven Jäger ermittelt, festgestellt, wie die Populations-Dichte
um ihn herum ist ("lebe_dichte""), und zuletzt die Position der
zu ihm nahesten Beute ermittelt ("leben_finde"").
Durch Aufruf der nun folgenden Funktion "leben_bewegung" lassen
wir unseren Jäger seine Position ändern, und zwar so, dass er sich
dabei möglichst auf die gefundene Beute zubewegt.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
//-------------------------------------------------------
//zufaelliger delta-wert, der zu x/y-koordinaten addiert
//oder subtrahiert wird, je nach Richtung
//-------------------------------------------------------
function thauptf.pos_delta:smallint;
begin
//0 oder ein 'schritt'
result:=random(2)*groesse_se.value;
end;
//-------------------------------------------------------
//bewege ein jaeger oder eine beute in zufaelliger weise,
//sodass sich jaeger moeglichst auf naheste beute zubewegt
//bzw. beute vom nahesten jaeger fortbewegt.
//-------------------------------------------------------
function thauptf.leben_bewegung(
var ar:array of TLeben;
inx,x_gefunden,y_gefunden:smallint;
jaeger_ok:boolean
):smallint;
var
x,y,delta_x,delta_y,index_gefunden:smallint;
begin
result:=_leben_frei;
//x: bewege dich auf beute zu bzw. von jaeger weg
x:=ar[inx].x;
delta_x:=0;
if x<x_gefunden then delta_x:=pos_delta
else if x>x_gefunden then delta_x:=-pos_delta;
if not jaeger_ok then delta_x:=-delta_x;
//y: bewege dich auf beute zu bzw. von jaeger weg
y:=ar[inx].y;
delta_y:=0;
if y<y_gefunden then delta_y:=pos_delta
else if y>y_gefunden then delta_y:=-pos_delta;
if not jaeger_ok then delta_y:=-delta_y;
//ortsaenderung gegeben?
if(delta_x=0)and(delta_y=0)then exit;
//berechne neue pixel-koordinaten
if delta_x<>0 then x:=pos_raster_x(x+delta_x);
if delta_y<>0 then y:=pos_raster_y(y+delta_y);
//neue position frei von leben?
index_gefunden:=_leben_frei;
if leben_vorhanden(ar,x,y,index_gefunden,jaeger_ok) then exit;
//ja, dann aendere position
//alte position im dichte-array freigeben
dichte_ar_setzen(ar[inx].x,ar[inx].y,jaeger_ok,true);
if jaeger_ok then begin
//hockt auf neuer position des jaeger eine beute?
//wenn ja, rette beute-index in dum
index_gefunden:=0;
if beute_vorhanden(x,y,index_gefunden) then
result:=index_gefunden;
end;
//neue position eintragen
ar[inx].x:=x;
ar[inx].y:=y;
dichte_ar_setzen(x,y,jaeger_ok,false);
end;
In den übergebenen Parametern "x_gefunden" und
"y_gefunden" stehen die Koordinaten der nahesten
Beute. In Abhängigkeit davon werden die Bewegungs-Deltas
"x_delta" und "y_delta" berechnet, die, aufsummiert
auf die aktuellen Koordinaten des Jägers, diesen näher an die
Beute heranführen sollen.
Die Delta werden über die Funktion "pos_delta" jedoch nicht
einfach exakt berechnet, sondern zusätzlich mit einem Zufallswert
belegt, sodass es unter Umständen vorkommen kann, dass der Jäger
sich gar nicht bewegt, sondern dort bleibt, wo er sich gerade befindet.
In diesem Fall können wir die Funktion sofort wieder verlassen.
Ansonsten wird mit der bereits vorgestellten Funktion
"leben_vorhanden" überprüft, ob sich auf der neu
berechneten Position bereits ein anderer Jäger befindet. Ist dem so,
bleibt der aktuelle Jäger ebenfalls sitzen. Wir verlassen auch hier
ohne jede weitere Änderung die Funktion.
Im anderen Fall wird die Positionsänderung jedoch tatsächlich
durchgeführt. Hierzu wird zunächst die alte Position im Dichte-Array
"dichte_ar" freigegeben, wobei die weiter oben vorgestellte
Prozedur "dichte_ar_setzen" aufgerufen wird. Anschliessend
wird mit der Funktion "beute_vorhanden" noch ünerprüft,
ob auf der neuen Position eventuell ein fettes Beute-Tier sitzt.
Ist dem so, dann steht im Parameter "index_gefunden" der
Array-Index des gefundenen Beute-Tiers. Erreicht wurde das übrigens
nur, weil wir zuvor diesen Parameter auf einen Wert ungleich "-1"
gesetzt haben (siehe "Existiert Leben an einem bestimmten Ort?").
Zuletzt wird die neue Position des Jägers in die Attribute "x"
und "y" eingetragen. Der erneute Aufruf von "dichte_ar_setzen"
sorgt zudem dafür, dass auch das Dichte-Array an passender Stelle mit dem
Jäger-Zeichen versehen wird.
Jagender Jäger I: Ein Jäger sucht nach der ihm nahesten
Beute - und findet ein Beute-Tier nur ein wenig links von sich
herumlungern. Das wird ab jetzt sein Ziel-Objekt sein.
Jagender Jäger II: Der Jäger hat seine Ziel-Beute fast erreicht,
obwohl dieses noch versucht hat, vor ihm zu fliehen. Die anderen Beute-Tiere
haben sich indessen an den Rändern des Modells vorerst in Sicherheit gebracht.
Zurück in die Prozedur "jaeger_bewegen". Wir haben jetzt einen
aktiven Jäger ermittelt, die Dichte um ihn herum festgestellt, dass
naheste Beute-Tier gefunden und den Jäger mittels "leben_bewegung"
gezielt in dessen Richtung laufen lassen.
Hier noch einmal ein Ausschnitt aus dem Source-Code von
"jaeger_bewegen":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
//beute gefunden: bewege jaeger dorthin
inx:=leben_bewegung(jaeger_ar,r,x_gefunden,y_gefunden,true);
//hat jaeger beute erwischt?
if inx=_leben_frei then continue;
//jep: lass beute sterben
beute_ar[inx].zaehler:=_leben_frei;
dichte_ar_setzen(beute_ar[inx].x,beute_ar[inx].y,false,true);
//erhoehe beutetreffer des jaegers
jaeger_ar[r].gefressen:=jaeger_ar[r].gefressen+1;
//genug beute gerissen fuer reproduktion?
if jaeger_ar[r].gefressen<_beute_fuer_duplikat then
continue;
//ja, jaeger dupliziert sich (falls moeglich)
if leben_duplikat(jaeger_ar,r,true,start_index) then
duplikat_ok:=true;
Wurde dabei die Position eines Beute-Tiers eingenommen, hat unser
Jäger also Beute schlagen können, so wird dessen Index-Wert in
"inx" zurückgeliefert.
Die Beute wird vertilgt, indem ihr Attribut "zaehler" auf
"_leben_frei" gesetzt wird. Das "gefressen"-Attribut
des Jägers wird hingegen um eins erhöht.
Hat der Jäger dadurch insgesamt "_beute_fuer_duplikat" Beute-Tiere
gefressen, dann kann er sich nun duplizieren. Dieser Prozess wird über
die Funktion "leben_duplikat" angestossen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
//-------------------------------------------------------
//ein jaeger oder eine beute reproduziert sich
//liefert true zurueck, wenn alles klappt
//-------------------------------------------------------
function thauptf.leben_duplikat(
var ar:array of tleben; //jaeger- oder beute-array
inx:smallint; //index des jaegers/der beute
jaeger_ok:boolean; //jaeger?
var start_index:smallint //start-index im array
):boolean;
var
r,x,y,max,dum:smallint;
found_ok:boolean;
begin
result:=false;
try
if jaeger_ok then max:=jaeger_max_se.value
else max:=beute_max_se.value;
//suche freie array-position
found_ok:=false;
for r:=start_index to max-1 do begin
//jaeger/beute aktiv?
if ar[r].zaehler>=0 then continue;
//freien index sichern
start_index:=r;
found_ok:=true;
break;
end;
if not found_ok then exit;
//zufallsposition in nachbarschaft
//des lebewesens, das sich repliziert
x:=ar[inx].x+pos_delta-pos_delta;
y:=ar[inx].y+pos_delta-pos_delta;
//position modulieren
x:=pos_raster_x(x);
y:=pos_raster_y(y);
//ort identisch mit original?
if(ar[inx].x=x)and(ar[inx].y=y)then exit;
//ort von anderem lebewesen besetzt?
dum:=_leben_frei;
if
beute_vorhanden(x,y,dum) or
jaeger_vorhanden(x,y,dum)
then exit;
//okay, duplizierung vornehmen
if jaeger_ok then
ar[start_index].zaehler:=jaeger_variation
else
ar[start_index].zaehler:=beute_variation;
ar[start_index].x:=x;
ar[start_index].y:=y;
ar[start_index].gefressen:=0;
//neues leben auch in dichte-array
dichte_ar_setzen(x,y,jaeger_ok,false);
result:=true;
finally
//aktives lebenwesen bekommt immer volle lebenskraft
if jaeger_ok then ar[inx].zaehler:=jaeger_variation
else ar[inx].zaehler:=beute_variation;
ar[inx].gefressen:=0;
end;
end;
Zuerst suchen wir im Jäger-Array "ar" nach einer freien
Position, dass heisst, nach einer "TLeben"-Instanz mit Attribut
"zaehler" kleiner Null.
Wurde kein freier Eintrag gefunden, dann kann sich der Jäger
nicht replizieren, da bereits die maximal mögliche Population
erreicht wurde. Wir verlassen die Funktion unverrichteter Dinge.
Ansonsten wird mithilfe der bereits bekannten Funktion pos_delta"
eine neue, gerasterte Zufalls-Position um den Jäger
herum ermittelt, auf dem sich das neue Leben "materialisieren" soll.
Bleibt zu überprüfen, ob dieser Ort nicht bereits von einem anderen
Leben besetzt worden ist ("beute_vorhanden" oder
"jaeger_vorhanden" liefern true zurück). Ist dem so,
dann verlassen wir die Funktion; die Duplizierung des Jägers ist
diesesmal aufgrund von Platzmangel fehlgeschlagen.
Ansonsten wird der neue Jäger an die freie Position gesetzt.
Und das Dichte-Array "dichte_ar" wird über die Prozedur
"dichte_ar_setzen" ebenfalls aktualisiert.
Egal, ob sich der aktuelle Jäger letztlich replizieren konnte
oder nicht, er erhält im "finally"-Teil in jedem Fall Lebenskraft
zurück, indem sein Attribut "zaehler" per
"jaeger_variation"-Aufruf neu gefüllt wird.
Fressende Jäger: Ein Rudel Jäger frisst sich von oben links
bis unten recht so erfolgreich durch die Population der Beute, das deren
Bestände dramatisch abnehmen, während die Zahl der Jäger durch Replikation
stetig zunimmt.
Wir befinden uns wieder in der Prozedur "evolution".
Gerade haben wir die Meute der aktiven Jäger auf die Beute
zubewegt, die Dichte ihrer Population ermittelt und gegebenfalls
einzelne Jäger nach erfolgreicher Jagt replizieren lassen.
Anhand dieser Daten können wir jetzt unsere Modell-Parameter
modifizieren. Die nächste Generation der Jäger soll etwas
lernen, und zwar derart, dass sich ihre Überlebenswahrscheinlichkeit
insgesamt erhöht.
Dafür kommt die Funktion "jaeger_lernen" zum Einsatz:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
//-------------------------------------------------------
//lass jaeger der naechsten generation etwas lernen
//-------------------------------------------------------
procedure thauptf.jaeger_lernen(
dichte:smallint; //aktuelle dichte
dichte_max:smallint; //maximal moegliche dichte
duplikat_ok:boolean //mindestens eine reproduktion?
);
var
dichte_proz:double;
begin
//wahrscheinlichkeit, dass lernschritt stattfindet
if random(100)>=jaeger_lern_se.value then exit;
//hat sich mindestens ein jaeger repliziert?
if duplikat_ok then begin
//verkuerze lebensdauer der naechsten generation
jaeger_lebensdauer_se.value:=jaeger_lebensdauer_se.value-1;
end;
//'korrektur' der maximal moeglichen dichte
//ein jaeger benoetigt x beute
dichte_max:=dichte_max div _beute_fuer_duplikat;
if dichte_max=0 then exit;
//wie viel prozent dichte haben die jäger von der
//modifizierten maximal möglichen dichte erreicht?
dichte_proz:=(dichte*100)/dichte_max;
//jaeger haben viele direkte nachbarn?
if dichte_proz>jaeger_lern_minus then begin
//ueberbevoelkerung:
//verkuerze lebensdauer der naechsten generation
jaeger_lebensdauer_se.value:=jaeger_lebensdauer_se.value-1;
end
//jaeger haben wenige direkte nachbarn?
else if dichte_proz<jaeger_lern_plus then begin
//unterbevoelkerung:
//erhoehe lebensdauer der naechsten generation
jaeger_lebensdauer_se.value:=jaeger_lebensdauer_se.value+1;
end;
end;
Zunächst wird eine Zufallszahl gezogen und überprüft,
ob diese sich unterhalb einer vom Benutzer vorgebenen
Grenze befindet (Wert des SpinEdits "jaeger_lern_se").
Dies sorgt dafür, die Jäger nicht immer, sondern nur mit einer
gewissen Wahrscheinlichkeit etwas lernen. Evolutionär kann es
nämlich durchaus sinnvoll sein, dass die Jäger nicht
sofort und jedes Mal auf die geänderten Umweltbedingungen
reagieren; Sturheit und Festhalten am Alten ist manchmal
"überlebensträchtiger" als übereiltes reagieren.
Anschliessend wird geprüft, ob sich mindestens einer der Jäger
hat reproduzieren können. Ist dem so, dann wird die
Lebensdauer der nächsten Generation, die im SpinEdit
"jaeger_lebensdauer_se" steht, generell um ein Jahr
verkürzt. Denn dieses Ergebnis deutet schliesslich an,
dass es noch genügend Beute für die Jäger gibt, sie sich also
nicht unedingt auf längere "Durststrecken" einstellen müssen.
Im nächsten Schritt wird die maximal mögliche Dichte der
Jäger "dichte_max" etwas modifiziert, sprich, verkleinert.
Wir teilen sie durch die Anzahl der Beute-Tiere, die jeder
Jäger fressen muss, bevor er sich replizieren kann. Das neue
Maximum lässt so gewissermassen genügend Raum für die Beute,
die nötig ist zum Überleben. Okay, dieser Korrektur-Term ist
ein wenig an den Haaren herbeigezogen, liefert aber für
das Modell bessere Werte.
Jetzt weisen wir der Variable "dichte_proz" den Prozenwert
zu, den die aktuelle Dichte von der modifizierten maximalen Dichte
erreicht hat.
Wir prüfen dann das Modell auf Überbevölkerung an Jägern. Diese
ist gegeben, wenn "dichte_proz" grösser ist als der in der
Konstanten "jaeger_lern_minus" gesetzte Prozentwert (bei
uns sind das 70%). In diesem Fall wird die Lebensdauer der nächsten
Generation der Jäger herabgesetzt. Das heisst also: Gibt es zu
einem bestimmten Zeitpunkt relativ viele Jäger, dann steigt die
Wahrscheinlichkeit, dass ihre Nachkommen weniger lang leben.
Auf gleiche Weise wird geprüft, ob "dichte_proz" kleiner
ist als die Konstante "jaeger_lern_plus" (=30%). Ist dem
so, dann gibt es eher zu wenige Jäger. Jetzt wird die Lebensdauer
der nächsten Generation der Jäger natürlich erhöht, um so ihre
Chancen auf Beute-Erfolg und damit vermehrte Replikation zu
erhöhen.
Lernen in Evolutions-Runde 64: Wir haben wenige Jäger und viel Beute
im Modell. Die Lebensdauer der Jäger ist daher hoch und die Teildauer der
Beute ebenfalls.
Lernen in Evolutions-Runde 439: Die Jäger drohen überhandzunehmen.
Daher ist ihre Lebensdauer bereits etwas gesunken. Die Teildauer der Beute hat
dagegen schon erheblich abgenommen, damit sie sich schneller replizieren kann.
Zurück in der Prozedur "evolution". Wir haben gerade die Jäger
bewegt, ihre Populations-Dichte ermittelt, eventuell Beute
getilgt und so neue Jäger generiert. Am Schluss haben wir anhand
von Populations-Dichte und Fress-Erfolg für die nächste Generation
der Jäger mit einer gewissen Wahrscheinlichkeit Konsequenzen in
Form einer geänderten durchschnittlichen Lebensdauer gezogen.
In ganz ähnlicher Weise wird nun auch mit der Beute verfahren,
jedoch mit genau umgekehrten Vorzeichen: Die Beute bewegt sich vom
jeweils nahesten Jäger fort und repliziert sich, wenn ihre Teildauer
überschritten wurde. Über- bzw. Unterbevölkerung und Teilungserfolg
bewirken schliesslich mit einer gewissen Wahrscheinlichkeit eine Änderung
der Teildauer der nächsten Generation der Beute.
Dazu kommen die Prozeduren "beute_bewegen" und "beute_lernen"
zum Einsatz, deren Erläuterung wir uns hier sparen können, da sie weitgehend
mit den Prozeduren "jaeger_bewegen" und "jaeger_lernen" für die
Jäger übereinstimmen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
//-------------------------------------------------------
//bewege die population der beute
//-------------------------------------------------------
procedure thauptf.beute_bewegen(
var dichte:smallint; //reale dichte um beute herum
var dichte_max:smallint; //maximal moegeliche dichte
var duplikat_ok:boolean //mindestens eine reproduktion?
);
var
r,start_index:smallint;
x_gefunden,y_gefunden:smallint;
dichte_einzeln,dichte_einzeln_max:smallint;
begin
start_index:=0;
duplikat_ok:=false;
dichte:=0;dichte_max:=0;
//durchlaufe beute-array
for r:=0 to beute_max_se.value-1 do begin
//ignoriere inaktive beute
if beute_ar[r].zaehler=_leben_unbenutzt then exit;
if beute_ar[r].zaehler=_leben_frei then continue;
//bestimme dichte um aktive beute
dichte_einzeln_max:=0;
dichte_einzeln:=leben_dichte(
beute_ar,r,false,dichte_einzeln_max
);
//aufsummierte dichte ueber alle beute-tiere
dichte:=dichte+dichte_einzeln;
dichte_max:=dichte_max+dichte_einzeln_max;
//bewegung ueberhaupt moeglich?
if dichte_einzeln=dichte_einzeln_max then continue;
//jep: also suche nach nahestem jaeger
if leben_finde(jaeger_ar,r,x_gefunden,y_gefunden,true)then begin
//jaeger gefunden: beute entfernt sich von ihm
leben_bewegung(beute_ar,r,x_gefunden,y_gefunden,false);
end;
//lebt beute lang genug fuer duplizierung?
if beute_ar[r].zaehler>0 then continue;
//ja: beute reproduziert sich (falls moeglich)
if leben_duplikat(beute_ar,r,false,start_index) then
duplikat_ok:=true;
end;
end;
//-------------------------------------------------------
//lass beute der naechsten generation etwas lernen
//-------------------------------------------------------
procedure thauptf.beute_lernen(
dichte:smallint; //aktuelle dichte
dichte_max:smallint; //maximal moegliche dichte
duplikat_ok:boolean //mindestens eine reproduktion?
);
var
dichte_proz:double;
begin
//wahrscheinlichkeit, dass lernschritt stattfindet
if random(100)>=beute_lern_se.value then exit;
//hat sich mindestens eine beute repliziert?
if duplikat_ok then begin
//erhoehe teildauer der naechsten generation
beute_teildauer_se.value:=beute_teildauer_se.value+1;
end;
//'korrektur' der maximal moeglichen dichte
//beue benoetigt freien platz zu reprouktion/flucht
dichte_max:=(dichte_max*3) div 4;
if dichte_max=0 then exit;
//wie viel prozent dichte hat die beute von der
//modifizierten maximal möglichen dichte erreicht?
dichte_proz:=(dichte*100)/dichte_max;
//beute haben viele direkte nachbarn?
if dichte_proz>beute_lern_plus then begin
//ueberbevoelkerung:
//erhoehe teildauer der naechsten generation
beute_teildauer_se.value:=beute_teildauer_se.value+1;
end
//beute haben wenige direkte nachbarn?
else if dichte_proz<beute_lern_minus then begin
//unterbevoelkerung:
//verringere teildauer der naechsten generation
beute_teildauer_se.value:=beute_teildauer_se.value-1;
end;
end;
Wir befinden uns wieder in der Prozedur "evolution". Gerade wurden
über "evolution_ausgabe" die aktiven Jäger und aktiven Beute-Tiere
auf dem Bildschirm ausgegeben. Dabei wurde auch ihre Anzahl ermittelt und
in die zugehörigen SpinEdits "jaeger_se" und "beute_se"
eingetragen.
Es folgt der Aufruf der Funktion "bremsen_pause_abbruch",
um festzustellen, ob der Benutzer den Evolutionsprozess manuell
abgebrochen hat, oder ob ein "natürliches" Ende erreicht wurde,
weil entweder Jäger und/oder Beute ausgestorben sind.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
//--------------------------------------------------------
//pausiere evolution um ms millisekunden
//--------------------------------------------------------
procedure Thauptf.pause(ms:smallint);
var
r:smallint;
begin
for r:=0 to (ms div 10) do begin
application.processmessages;
sleep(10);
end;
end;
//-------------------------------------------------------
//evolution abbremsen? pausieren? oder abbrechen?
//-------------------------------------------------------
function thauptf.bremsen_pause_abbruch:boolean;
begin
//bremsen?
if bremse_se.value=0 then begin
application.processmessages;
end
else pause(bremse_se.value);
//pause?
if pauseb.caption='Weiter' then begin
startb.enabled:=false;
while pauseb.caption='Weiter' do begin
application.processmessages;
sleep(10);
end;
startb.enabled:=true;
end;
//ende oder abbruch?
result:=(startb.caption<>'STOPP');
end;
Hat der Benutzer im SpinEdit "bremse_se" einen Wert
grösser Null eingetragen, wird die Prozedur "pause"
aufgerufen und der Evolutionsprozess kurzfristig angehalten,
bevor automatisch fortgefahren wird. Das bewirkt eine Art
Zeitlupen-Effekt. Dadurch kann man leichter verfolgen, was
sich auf dem Bildschirm alles abspielt.
Über die Caption des Schalters "pauseb" stellt das
Programm fest, ob der Benutzer in den Pause-Modus zu gehen
wünscht. Ist dem so, wird die innere Schleife so lange
durchlaufen, bis der Pause-Knopf erneut gedrückt wird und
seine Beschriftung von "Weiter" wieder auf "Pause" wechselt.
Pause-Schalter: Der Pause-Modus ist links inaktiv, recht aktiv.
Als Ergebnis liefert die Funktion "true" zurück, sowie
die Caption des Start-Knopfes "startb" nicht länger "STOPP"
sondern wieder "Start" ist.
Zum Schluss gibt 's noch ein paar Screenshot und ein Flash-Movie
von gerade laufenden Evolutions-Prozessen aus "Pixel-Evolution".
Jäger in der Übermacht: Die Jäger dominieren im Modell. Sie konzentrieren
sich auf die letzten beiden "Inseln" mit Beute-Tieren. Das Raumangebot ist
jedoch so gross, dass sich die Beute eventuell noch zu retten vermag, und
dann mit verkürzter Teildauer ihre Population rasch anwachsen lässt. Die
Lebensdauer der Jäger hat sich bereits halbiert, wird aber bis dahin noch
weiter gesunken sein, sodass sie unter Umständen Probleme haben werden,
die neue Beute rechtzeitig zu erreichen.
Reiche Futtergründe: Die Beute dominiert im Modell. Hier gibt es
also reichlich Futter für die bisher zahlenmässig stark unterlegen Jäger.
Ein grosses Raumangebot im Modell bietet tendenziell vor allem der Beute
Vorteile, da sie hier stets über genügend Fluchtmöglichkeiten verfügt.
Keine Flucht-Option: Die Beute dominiert auch in diesem Modell.
Jedoch verfügt sie über kaum Fluchtmöglichkeiten. Die Jäger werden sich
rasch vollgefressen und dupliziert haben. Selbst bei verkürzter Teildauer
wird es wohl schwer werden für die Beute, in diesem Umfeld dauerhaft bestehen
zu können.
Angriffs-Strategien: Obwohl die Jäger nur über minimale Intelligenz
verfügen und sich untereinander in keiner Weise miteinander absprechen,
sind sie dennoch in der Lage für einen Aussenstehenden den Eindruck einer
gemeinsamen Strategie zu vermitteln, um das für sich beste Ergebnis zu
erreichen. Es handelt sich hierbei wohl um das Phänomen, welches man als
"Schwarm-Intelligenz" bezeichnet (mh ... nein, da es sich um Jäger
handelt, wäre der Ausdruck "Meute-Intelligenz" passender). Die
folgenden drei Beispiele mögen dies verdeutlichen:
Zwei-Fronten-Angriff (Strategie I): Zwei relativ kleine Gruppen
von Jäger haben sich weit voneinander getrennt und greifen nun von oben
und unten gleichzeitig die Beute an. Die Beute wird "instinktiv" zur
Mitte hin ausweichen, da sich dort (noch) keine Jäger befinden.
Zangen-Angriff (Strategie II): Eine Forsetzung von Strategie I kann
der Zangen-Angriff sein, wenn es der Beute beim Zwei-Fronten-Angriff gelingt,
zur Mitte hin auszuweichen. Wie als hätten sie es gemeinsam geplant, zwingen
die Jäger dabei die Beute zu einem Haufen zusammen, sodass diese umzingelt
wird. Die Jäger brauchen sich durch diesen Haufen nur noch durchzufressen.
In-die-Ecke-drängen (Strategie III): Sollte ein Teil der Beute auch der
Strategie II entkommen sein, so könnten die Jäger nun mit Strategie III
fortfahren und die letzten Beute-Tiere in eine Ecke des Modells drängen. Da
sie - wie im obigen Beispiel - auf breiter Front angreifen, bleiben hier
kaum mehr Fluchtmöglichkeiten für die Beute offen. Ihre einzige Chance ist,
dass sich im anschliessenden Tumult eines der Beute-Tiere übersehen wird.
Dieses wird sich dann rasch von den Jägern entfernen und im sicheren Abstand
eine neue Population aufbauen.
Penicillin versus Bakterien: Zuletzt noch zwei
Beispiele, bei denen die Beute über viel Lebensraum verfügt und zunächst nur wenige
Jäger aktiv sind. Das Bild, dass sich im Modell ergibt, ähnelt in verblüffender Weise
dem, was sich in einer Petrischale mit Bakterien abspielt, in die ein
Fressfeind wie Penicillin eingeführt wurde.
Beute-Strukturen: Die Beute-Tiere (Bakterien) suchen das für sich
angenehmste Milieu auf. Im Modell heisst das, möglichst weit vom jeweils nächsten
Jäger (Penicillin) entfernt. Dadurch bilden sich netzartige Strukturen
des Lebens, die um die Fressfeinde herum verlaufen.
Weites Land: Die Fressorgien der Jäger (Penicillin) konzentrieren
sich häufig ausschliesslich auf die naheliegendste Beute (Bakterien).
Hier im Modell ist das unten Links. Dadurch gelingt es der Beute mit unter
sich aus der Umklammerung zu lösen und weiter entfernt - ungestört von
den Jägern - eine neue Population aufzubauen. Hier ist das oben rechts
gelungen. Ist die Entfernung nur weit genug, kann es sogar sein, dass die
Jäger anschliessend die neue Population nicht mehr rechtzeitig erreichen
können - und somit vollständig aussterben.
Bakterien-Stämme des realen Lebens: Wir sehen hier offenbar zufällige
Verteilungen von Bakterien, sowie netzartige Strukturen, die Ähnlichkeit zu
denen haben, die wir bei "Pixel-Evolution" beobachten können. Reale
Bakterien scheinen sich also in mancher Hinsicht ähnlich "intelligent" zu
verhalten, wie unsere simulierte Beute.
|
Screen-Movie von Pixel-Evolution in Action
|
Wie Eingangs erwähnt basiert "Pixel-Evolution" auf einem alten
Source-Code, den ich vor ein paar Jahren in einem eigenen, kleinen
Bildschirm-Schoner verwendet habe. Damals ist es mir aber nicht gelungen,
ein wirklich autark ausbalanciertes Verhältnis von Jägern und Beute
zu erreichen. Das alte Modell funktionierte nur dann, wenn die
Anzahl der Jäger und der Beute von vorneherein stark begrenzt war,
wobei zusätzlich sogar noch ein bestimmtes Verhältnis der Gattungen
zueinander fixiert werden musste.
Die genauen Zahlen waren: Maximal 10 Jäger und maximal 100 Beute-Tiere.
Auf einen Jäger kamen also bis zu 10 Beute-Tiere. Die Population war auf
beiden Seiten künstlich begrenzt, sodass letztlich keine der beiden
Gattungen wirklich dominieren konnte.
Auch in "Pixel-Evolution" können solche Grenzwerte festgelegt
werden, wenn es der Benutzer wünscht. Viel spannender ist es jedoch,
"natürlich" vorzugehen und keine (bzw. nur eine sehr hohe) Obergrenze
für die Populationen von Jäger und Beute festzulegen. Erst dadurch erlebt
man live, wie sich das ganze System durch nur wenige "genetische" Regeln
selbst in Balance halten kann.
Dabei wurde übrigens weitgehend auf "Meta-Wissen" verzichtet. Die
Tiere kommunizieren nämlich nicht miteinander. Sie reagieren nur auf ihr
unmittelbar "wahrnehmbares" Umfeld. Jedes Tier kämpft nur für sich,
fast so wie im wahren Leben :-)
Nun ja, im Vergleich zur Natur wissen die Tiere hier vielleicht doch etwas
zu viel. So sind sie z.B. in der Lage, Entfernungen zur nahesten Beute bzw.
zum nahesten Jäger exakt zu berechnen. So genau könnte das ein reales
Tier natürlich nicht.
Ausserdem musste ich für die Anwendung der Lern-Regeln zur
"festgestellten" Populations-Dichte, die jedes Tier für sich wahrnimmt
und so durchaus auch in der Natur möglich ist, zusätzlich noch die
maximal mögliche Dichte berechnen, um dies in die Lebensdauer bzw.
Teildauer der nächsten Generationen einfliessen lassen zu können. Okay,
das ist dann doch eine Art Meta-Wissen, dass so im wahren Leben wohl nur
vernunftbegabte Tiere wie etwa Menschen erfassen könnten.
Insgesamt gesehen musste ich als Programmierer jedoch nur wenig
steuernde Regeln einarbeiten, um das System im Gleichgewicht zu halten.
Das Ganze erweist sich dadurch als ein teilweise extrem dynamischer
Prozess, bei dem man oft eine Gattung auf der Verliererseite sieht,
diese sich aber im letzten Moment dann doch noch erholt und die Bestände
der eigenen Gattung aufzufüllen versteht.
Leider vermag das Modell jedoch nicht, den wahren Vorteil dieser Dynamik
zu offenbaren. Denn man hätte sich ja auch leicht ein System schaffen
können, bei dem sich die Beute-Tiere mit sehr hoher Geschwindigkeit
replizieren, während die Jäger sich nur sehr langsam hätten vermehren
können. In diesem Szenario würden die Jäger praktisch nie überhandnehmen
und also auch die Beute nie aussterben. Folglich wäre ein solches System
ebenfalls ausbalanciert gewesen.
Aber es wäre bis zu einem gewissen Grad halt nur ein sehr statisches
System dabei herausgekommen. Weder Jäger noch Beute würden je ihr
Verhalten bzw. ihre Gene ändern müssen, um den Fortbestand ihrer Gattung
zu sichern. Es fände schlicht keine Evolution statt. Und vielleicht noch
schwerwiegender: Dieses Modell wäre völlig unfähig, sich auf plötzlich
ändernde Umweltbedingungen einzustellen.
Würde man hier etwa einen simulierten Meteoriten hineinplatzen
lassen, der mit einem Schlag einen Grossteil der Beute eliminiert, würde
das vermutlich nach nur wenigen weiteren Evolutions-Schritten alles
Leben aussterben lassen.
Wie gesagt, einen solchen Meteoriten-Einschlag kann das Modell
leider nicht simulieren. Auch gibt es keine Verstecke für die
Beute, oder auch nur wenigstens eine Einschränkung des Anstiegs ihrer
Population durch ein wie auch immer geartetes knapper werdendes
Nahrungsangebot.
So etwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy.
Ist halt nichts 100%iges. But who cares?
"Pixel-Evolution" wurde in Delphi 7 programmiert. Im ZIP-File
enthalten ist der vollständige Source-Code, sowie die EXE-Datei. Das
Paket, etwa 240 kB, gibt's hier:
Pixel-Evolution.zip
Es wurde auf die Verwendung von Fremd-Komponenten verzichtet. Auch werden
keine speziellen DLLs benötigt. Der Source-Code lässt sich sicher leicht auf
andere Delphi-Versionen anpassen. Das ausführbare Programm ist mit 450 kB
recht anspruchslos. Ausserdem nimmt es keinerlei Änderungen an der Registry
vor; alle Programm-Parameter werden über eine INI-Datei im Arbeitsordner
verwaltet.
Ach ja, mein persönlicher "Evolutions-Runden-Rekord" liegt übrigens
bei über 20.000 (Jahren)! Wer vermag das zu toppen?
Have fun!