Pixel-Evolution - Jäger und Beute ausbalanciert

Pixel-Evolution-Tutorial von Daniel Schwamm (03.04.2009)

Inhalt

1. Lebens am PC simulieren?

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.

Pixel-Evolution - Prähistorischer Jäger und seine Beute

2. Delphi-Projekt "Pixel-Evolution"

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.

2.1. Die Bausteine des Lebens

Alles spielt sich in nur einer Unit und auf einem Formular ab.

Pixel-Evolution - Das Haupt-Formular in Delphi

"hauptf": Das Hauptf-Formular von "Pixel-Evolution".

2.1.1. Die Konstanten

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';
2.1.1.1. Populations-Dichte

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.

2.1.1.2. Teilungszyklen

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.

2.1.1.3. Lebensdauer

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).

2.1.1.4. Geschlechtsreife

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.

2.1.1.5. Ausbalancierte Verhältnisse

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 :-)

2.1.2. Deklaration von "TLeben"

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;
2.1.2.1. Position

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.

2.1.2.2. "Genetische" Lebensdauer

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.

2.1.2.3. "Genetische" Teildauer

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.

2.1.2.4. Fress-Zähler

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.

2.1.3. Die Variablen

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;
2.1.3.1. Zwei verschiedene Lebensformen

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.

2.1.3.2. Begrenzter Lebensraum

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').

Pixel-Evolution - Jäger, Beute und Dichte

Visualisierung: Grafische Wiedergabe der obigen Arrays von Jäger und Beute.

2.2. Formelles

2.2.1. FormCreate()

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.

2.2.2. FormDestroy()

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;

2.2.3. INI-Datei mit Konfigurations-Parametern

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

2.3. Evolutions-Schleife

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.

Pixel-Evolution - Start und Ende des Evolution-Prozesses

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.

2.3.1. Button-Handling - Krieg der Knöpfe

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.

Pixel-Evolution - Knöpfe vor/nach der Evolution

Knöpfe I: Die Schalter und SpinEdits vor bzw. nach der Evolution.

Pixel-Evolution - Knöpfe während der Evolution

Knöpfe II: Die Schalter und SpinEdits während der Evolution.

2.3.2. Initialisierung der Evolutions-Umgebung

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.

2.3.3. Grössen-Anpassung

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            *
  ********************************

Pixel-Evolution - Grössen-Anpassung

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.

2.3.4. Zufällig Leben erschaffen

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.

2.3.5. Existiert Leben an einem bestimmten Ort?

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;

2.3.6. Gelernte Variationen

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;
2.3.6.1. Jäger-Mutationen - variable Lebensdauer

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.

2.3.6.2. Beute-Mutationen - variable Teildauer

In ähnlicher Weise wird die Teildauer der Beute variiert.

Pixel-Evolution - Variation der Lebenskraft

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.

2.4. Visualisierung des Modells

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.

2.4.1. Bewegung im Raster-Gitter

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.

Pixel-Evolution - Ungerastert und gerastert

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.

2.4.2. Jäger und Beute ausgeben

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.

Pixel-Evolution - Visualisierung des Modells

Visualisierung des Modells: Jäger und Beute kurz nach Evolutionsbeginn. Alle Tiere sind zufällig positioniert worden. Jetzt beginnt das grosse Fressen.

2.4.3. "TLeben"-Instanzen in Puffer-Bitmap platzieren

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;
2.4.3.1. Farbänderungen je nach Lebensstatus

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).

2.4.3.2. Aktives Leben erkennen

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.

2.4.3.3. Zählersummen

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.

2.4.3.4. Indizes der Dichte berechnen

Anhand der Position des aktuellen Lebens können die Indizes des Dichte-Arrays "dichte_ar" berechnet werden. Dort wird die gefundene Lebensform entsprechend eingetragen.

2.4.3.5. Rote Jäger, grüne Beute

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.

Pixel-Evolution - Jäger-Symbol  =  Pixel-Evolution - Fuchs als Jäger

Jäger/Fuchs: Unser stolzer Jäger, einmal als Symbol, einmal in Natura

Pixel-Evolution - Beute-Symbol  =  Pixel-Evolution - Hase als Beute

Beute/Hase: Unsere flinke und reproduktionsfreudige Beute im Spiel

2.5. Jäger-Intelligenz - die Meute hetzt die Beute

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.

Pixel-Evolution - Jäger bewegen sich auf Beute zu

Jäger-Bewegung I: Die Beute am unteren Rand fürchtet sich vor der Übermacht der anrückenden Jäger

Pixel-Evolution - Jäger haben Beute fast erreicht

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.

2.5.1. Wie eng geht es zu?

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.

Pixel-Evolution - Jäger-Populations-Dichte niedrig

Jäger-Dichte niedrig: Die Dichte der Population der Jäger ist relativ niedrig

Pixel-Evolution - Jäger-Populations-Dichte hoch

Jäger-Dichte hoch: Die Dichte der Population der Jäger ist ausgesprochen hoch

2.5.2. Jäger auf der Suche nach Leben

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.

2.5.3. Ein Jäger bewegt sich auf die naheste Beute zu

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.

Pixel-Evolution - Jäger beginnt (bestimmte) Beute zu jagen

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.

Pixel-Evolution - Jäger hat Ziel-Beute fast erreicht

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.

2.5.4. Das grosse Fressen

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.

Pixel-Evolution - Jäger fressen sich durch die Beute-Population

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.

2.5.5. Lernende Jäger

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.

Pixel-Evolution - Lernen-Grafik in Evolutions-Runde 64

Pixel-Evolution - Lernen-SpinEdits in Evolutions-Runde 64

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.

Pixel-Evolution - Lernen-Grafik in Evolutions-Runde 439

Pixel-Evolution - Lernen-SpinEdits in Evolutions-Runde 439

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.

2.6. Beute-Intelligenz - das gleiche Spiel mit umgekehrten Vorzeichen

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;

2.7. End of Evolution?

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.

Pixel-Evolution - Pause-Schalter aus    Pixel-Evolution - Pause-Schalter an

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.

3. Impressionen

Zum Schluss gibt 's noch ein paar Screenshot und ein Flash-Movie von gerade laufenden Evolutions-Prozessen aus "Pixel-Evolution".

3.1. Screenshots

Pixel-Evolution - Jäger in der Übermacht

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.

Pixel-Evolution - Reiche Futtergründe

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.

Pixel-Evolution - Keine Flucht-Option

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:

Pixel-Evolution - Strategie I: Zwei-Fronten-Angriff der Jäger

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.

Pixel-Evolution - Strategie II: Zangen-Angriff der Jäger

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.

Pixel-Evolution - Strategie III: Beute in die Ecke gedrängt

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.

Pixel-Evolution - Beute-Strukturen

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.

Pixel-Evolution - Weites Land

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.

Pixel-Evolution - Echte Bakterien #1 Pixel-Evolution - Echte Bakterien #2 Pixel-Evolution - Echte Bakterien #3 Pixel-Evolution - Echte Bakterien #4 Pixel-Evolution - Echte Bakterien #5 Pixel-Evolution - Echte Bakterien #6

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.

3.2. Flash-Demo

Screen-Movie von Pixel-Evolution in Action

4. Ein paar Schlussbemerkungen

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?

5. Download

"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!