14.12.2012 Aufrufe

2. Von objektorientierten Ameisen

2. Von objektorientierten Ameisen

2. Von objektorientierten Ameisen

MEHR ANZEIGEN
WENIGER ANZEIGEN

Sie wollen auch ein ePaper? Erhöhen Sie die Reichweite Ihrer Titel.

YUMPU macht aus Druck-PDFs automatisch weboptimierte ePaper, die Google liebt.

<strong>2.</strong> <strong>Von</strong> <strong>objektorientierten</strong> <strong>Ameisen</strong><br />

Die Idee<br />

Was das Kapitel OOP im Unterricht oft unerfreulich macht, ist die Tatsache, dass<br />

man mit wenig attraktiven Beispielen (die zudem auch ohne OO viel schneller<br />

programmiert worden wären!) und mit einer Unmenge abstrakter Begriffe selbst<br />

interessierte Schülerinnen und Schüler jeglichen Spass raubt. Das einführende, aber<br />

unvermeidliche Kapitel 5.1 war ja schon an der Grenze der Zumutbarkeit!<br />

Was kann man dagegen tun?<br />

Man bräuchte ein interessantes Programm, dass in den wesentlichen Teilen schon<br />

fertig geschrieben ist, im zu ergänzenden Teil aber die „volle Objektorientiertheit“<br />

fordert. Die Lernenden sollten sich nahezu ausschließlich auf die OOP konzentrieren<br />

können und dabei eigenes Experimentieren ermöglichen.<br />

Hier wird man fündig: http://antme.net/<br />

Der im Bild unten gezeigte Screenshot des Simulationsspiels AntMe! (frei übersetzt<br />

etwa: Mach mir die Ameise!) zeigt den <strong>Ameisen</strong>bau der „aTomKampfameisen“ ganz<br />

zu Beginn der Spielrunde. Weiß sind die Zuckerhaufen, grün die Obststücke, die den<br />

<strong>Ameisen</strong> als Nahrung dienen. Die etwas größeren blauen Punkte sind böse Käfer,<br />

die die <strong>Ameisen</strong> fressen wollen.<br />

Zugegeben, - die Grafik wirft einen nicht gerade um! Aber darauf kommt es hier auch<br />

gar nicht an. Sie sollen mit OOP möglichst intelligente <strong>Ameisen</strong> erschaffen<br />

Das AntMe!-Programm ist kostenlos und hat alle oben genannten erwünschten<br />

Eigenschaften. Wir werden in diesem Kapitel, eine eigene Klasse Powerameise<br />

16


aus der schon fertigen Klasse Ameise ableiten. Die daraus gebildeten Objekte<br />

sollen möglichst intelligent sein, so dass sie in der Simulation AntMe! auch gegen<br />

schon fertige <strong>Ameisen</strong>-Objekte eine Chance haben. (Künstliche Intelligenz ist hier<br />

das passende Stichwort!)<br />

Holen Sie sich den Ordner AntMe11 aus dem Tauschverzeichnis. Im Ordner<br />

befinden sich zwei weitere Ordner und einige Hilfedateien. Wir benötigen den Ordner<br />

AntMeEinsteiger. Öffnen Sie mit Visual C# 2008 die Projektdatei Spieler.sln.<br />

(Ärgerlich, aber typisch für die Kurzlebigkeit von Software: Eine Woche nach Fertigstellung dieses<br />

Skripts kam die neue Version AntMe! 1.6 herraus. Prinzipiell können alle Erkenntnisse aus AntMe! 1.1<br />

eins zu eins auf Version 1.6 übertragen werden. Die Bezeichnungen und die Gewinn-Punkte haben<br />

sich zwar teilweise geändert, das Spielkonzept ist aber gleich geblieben. Es steht jedem frei, die hier<br />

dargestellten Vorgehensweisen auf die neue Version zu übertragen. Allerdings sind die erreichten<br />

Punkte dann nicht mehr vergleichbar!)<br />

Ein erster Versuch<br />

Damit Sie gleich mit einer eigenen <strong>Ameisen</strong>art loslegen können, sind einige<br />

Kleinigkeiten noch zu erledigen.<br />

Der Proketmappen-Explorer zeigt am Anfang nur<br />

eine eine cs-Datei: Vorlage.cs. Da diese Datei<br />

unseren eigenen Quellcode enthalten soll, ist es<br />

sinnvoll, gleich eine Kopie der Datei zu erstellen.<br />

Wir nennen sie Spieler.cs.<br />

In der Datei sind schon viele, bisher noch leere<br />

Methoden vorbereitet.<br />

Der Namespace AntMe.Spieler ist ebenfalls schon<br />

vorbereitet. Wir müssen nur einen eindeutigen<br />

Namen für den Spieler wählen. (Vorname und<br />

Nachname können auch leer gelassen werden.)<br />

namespace AntMe.Spieler {<br />

[Spieler(<br />

Name = "Power<strong>Ameisen</strong>",<br />

Vorname = "Otto",<br />

Nachname = "Ameise"<br />

)]<br />

Im Folgenden ist dunkelblaue Schrift immer ein Zeichen dafür, dass die passende<br />

OOP-Theorie zum gerade behandelten konkreten Beispiel erklärt wird.<br />

Theoretisch hätte man den namespace Spieler auch innerhalb des namespace<br />

AntMe seperat deklarieren können:<br />

namespace AntMe<br />

{<br />

…...<br />

17


}<br />

namespace Spieler<br />

{<br />

…....<br />

}<br />

Die erste Definition ist aber etwas übersichtlicher: Erst kommt der übergeordnete<br />

namespace, dann ein Punkt und danach der untergeordnete namespace.<br />

In unserer Spieler.cs-Datei könnte man sogar nur<br />

namespace Spieler<br />

schreiben. Das liegt daran, dass im using-Abschnitt bereits der namespace AntMe<br />

eingetragen ist. So kann der Compiler die Namensergänzung vornehmen. Besonders<br />

übersichtlich ist diese Methode aber nicht!<br />

Wenn Sie das Spiel jetzt starten, bekommen Sie eine Fehlermeldung:<br />

Das wird Sie vermutlich erstaunen, denn so sehr Sie das Dokument auch<br />

durchsuchen: Eine Klasse innerhalb des namespace AntMe.Spieler mit Namen<br />

MeineAmeise können Sie nicht entdecken.<br />

Hier spätestens müssen Sie wissen, dass die Definition von namespaces sich auch<br />

über mehrere Dokumente verteilen kann.<br />

Diskutieren wir nicht lange mit dem Compiler und wählen einfach einen anderen<br />

Klassennamen: MeinePowerAmeise.<br />

public class MeinePowerAmeise : Ameise<br />

MeinePowerAmeise erbt, wie Sie wissen, alles von der bereits Definierten Klasse<br />

Ameise. Dass dies nicht sehr viel ist, können Sie sofort testen:<br />

Wenn man mit Strg+F5 startet, dann läuft der „Zeit-Ticker“ gleich mit an. Stoppen Sie<br />

den Vorgang, indem Sie auf das kleine grüne Quadrat oben links klicken. Nun<br />

können Sie über den Tab lokales Spiel eine <strong>Ameisen</strong>klasse auswählen:<br />

18


Klicken Sie auf Ihre Powerameise und starten Sie die Simulation mit dem kleinen<br />

grünen Button oben links. Wenn Sie den Haken bei konstante Bildrate rausnehmen,<br />

läuft die Simulation in maximalem Tempo.<br />

Große Enttäuschung: Ihre Powerameisen bleiben einfach im Bau und werden eine<br />

nach der anderen von den Käfern gefressen.<br />

(Im Bild rechts unten sieht man gerade noch ihre Fühler!)<br />

Ein Blick auf die Datei Spieler.cs hilft weiter. Dort sind die Gerüste vieler Methoden<br />

eingetragen. Wir müssen sie nur noch mit sinnvollen Anweisungen füllen.<br />

In Kapitel 2 haben Sie bestimmte Ereignismethoden von Steuerelementen in .NET<br />

kennengelernt. Zu einem Button ist zum Beispiel ein Click-Ereignis definiert. Hierzu<br />

gehört eine Methode, die beschreibt, was passieren soll, wenn der Benutzer auf den<br />

Button klickt. Der Benutzer selbst ist hier der Auslöser für das Click-Ereignis.<br />

In der AntMe!-Simulation greift der Benutzer (=der Spieler) nicht ein. Er kann sie<br />

höchstens starten oder beenden. Dennoch gibt es in der <strong>Ameisen</strong>klasse zum<br />

Beispiel eine Wartet()-Methode. Diese Ereignismethode wird ausgeführt, wenn eine<br />

Ameise inaktiv ist. In der Simulation läuft ein „Ticker“ mit: In jeder Sekunde werden<br />

eine bestimmte Anzahl von Runden durchgeführt. Bei jeder neuen Runde werden<br />

alle definierten Ereignismethoden überprüft und gegebenenfalls durchgeführt.<br />

Wir müssen einer wartenden Ameise daher mitteilen, was sie zu tun hat:<br />

public override void Wartet()<br />

{<br />

GeheGeradeaus();<br />

}<br />

Was public bedeutet sollten Sie noch wissen: <strong>Von</strong> überall kann auf diese Methode<br />

zugegriffen werden.<br />

Mit override wird die Wartet()-Methode überschrieben. Sie ist also bereits von der<br />

<strong>Ameisen</strong>klasse geerbt.<br />

Das dritte Wort void signalisiert, dass die Methode Wartet() keinen Wert zurückgibt.<br />

Die Methoden, die dem <strong>Ameisen</strong>konstrukteur zur Verfügung stehen, sind in der<br />

Spieler.cs-Datei zu finden. Damit etwas sinnvolles passiert, müssen sie<br />

überschrieben werden, - so wie die Waretet()-Methode oben. Daher steht<br />

vorsorglich schon immer override dabei.<br />

Die möglichen Befehle finden Sie in der dem AntME11-Ordner beiliegenden<br />

Befehlsliste.pdf – Datei. Hier ein Ausschnitt:<br />

19


Aufgabe 1<br />

Testen Sie, ob sich durch die obige Code-Änderung die Situation der Power<strong>Ameisen</strong><br />

verbessert hat. Welche Befehle und Methoden lassen sich denn noch sinnvoll<br />

kombinieren?<br />

public override void Sieht(Zucker zucker)<br />

{<br />

}<br />

Überschreiben was nicht passt<br />

Da die <strong>Ameisen</strong> hungrig sind, müssen wir die obige Methode überschreiben. Die<br />

Sieht()-Methode hat hier im Gegensatz zur Wartet()-Methode einen Parameter<br />

mitgebracht. Es handelt sich um ein Objekt names zucker der Klasse Zucker.<br />

Die Ereignismethode tritt also nur ein, wenn das von der Ameise gesehen Objekt ein<br />

zucker-Objekt ist.<br />

(Üblicherweise werden in .NET Objekte klein und Klassen groß geschrieben).<br />

Ergänzung:<br />

Zwischen den Klammern einer Methode dürfen auch mehrere Parameter stehen.<br />

Sie werden dann per Komma getrennt. Z.B.: Schnarchen(Int Lautstaerke, Int Dauer)<br />

Da die aufrufende Sieht()-Methode gleich auch als Parameter ein konkretes zucker-<br />

Objekt mitbringt, liegt der folgende Befehl nah:<br />

public override void Sieht(Zucker zucker)<br />

{<br />

GeheZuZiel(zucker);<br />

}<br />

Wenn die Ameise schon beladen ist, dann soll sie den Befehl ignorieren. Das<br />

erledigen wir mit einer Verzweigung. Die Befehlsliste hilft, ein hier relevante Feld<br />

(vom Typ bool) zu finden: AktuelleLast. Die IntelliSens zeigt, dass wir auf dem<br />

richtigen Weg sind:<br />

20


public override void Sieht(Zucker zucker)<br />

{<br />

if (AktuelleLast==0)<br />

{<br />

GeheZuZiel(zucker);<br />

}<br />

}<br />

Beim Testen stellen wir fest, dass nun alle <strong>Ameisen</strong>, die einen Zuckerhaufen<br />

gefunden haben, an diesem gebannt stehenbleiben. Sehr zur Freude der Käfer!<br />

Also müssen wir nach einer Methode suchen, die den <strong>Ameisen</strong> vorschreibt, was sie<br />

zu tun haben, wenn sie das Zucker-Ziel erreicht haben. Hier ist sie:<br />

public override void ZielErreicht(Zucker zucker)<br />

{<br />

}<br />

Danach sehen Sie in die Befehlsliste und entscheiden, welche Befehle hier sinnvoll<br />

sein könnten.<br />

Zunächst muss die Ameise den Zucker erst einmal aufnehmen. Hier passt der Befehl<br />

nimm(). Wenn man den Befehl eintippt, erkennt man, dass nimm() offensichtlich zwei<br />

verschiedene Parametertypen zulässt:<br />

Die erste Möglichkeit wäre (Obst obst) gewesen. Klar, - die <strong>Ameisen</strong> können sich<br />

von den rumliegenden (grünen) Obststücken ebenso ernähren, wie von Zucker.<br />

Wenn sie einen solchen Apfel nachhause bringen, bekommen sie 250 Punkte<br />

gutgeschrieben. Für Zuckerstücke gibt es nur 5 Punkte. Allerdings können sie einen<br />

Apfel nicht alleine tragen. Das macht die Sache komplizierter!<br />

21


Überladen ist bequem<br />

In Kapitel 5.1 haben wir „überladene“ Methoden behandelt. Hier an diesem Beispiel<br />

können Sie Ihre Kenntnisse darüber vertiefen.<br />

In Programmiersprachen, wie PHP, muss man darüber kein Wort verlieren. Diese<br />

Sprachen sind bequem zu handhaben, weil sie nicht typensicher sind. Definiert<br />

man dort eine Methode NimmMal(Int a, Int b), die das Produkt der Parameter a und<br />

b berechnen soll, so meckert der Interpreter nicht, wenn man die Methode mit<br />

double-Variablen füttert. Das klingt sinnvoll. Allerdings meckert er ebensowenig,<br />

wenn man für a den String „123“ und für b die Integerzahl 2 wählt, also „123“ * <strong>2.</strong> Das<br />

Ergebnis wird die Integerzahl 246 sein.<br />

Wenn man auch dies noch für sinnvoll hält, dann stutzt man mit Sicherheit, wenn<br />

NimmMal(hallo, nasowas!) ein Ergebnis, wie 0 ausgibt. Spätestens jetzt wird klar,<br />

dass Typensicherheit eine sehr wichtige Eigenschaft der Programmiersprachen ist.<br />

Der Compiler von C# lässt nicht einmal zu, wenn man für die Variablen double-<br />

Größen verwendet und bewahrt uns dadurch vor schwer zu entdeckenden Fehlern.<br />

Trotzdem kann man auch in C# und .NET Methoden für verschiedene Parameter fit<br />

machen. Man verwendet den gleichen Methodennamen Nimm() innerhalb der<br />

Klasse MeinePowerAmeise für verschiedene Methoden. Dieser Vorgang wird, wie<br />

Sie wissen, überschreiben von Methoden genannt.<br />

(Eigentlich werden ja nicht die Methoden Nimm() und ZielErreicht() überschrieben, sondern deren<br />

Bezeichner Nimm und ZielErreicht.…)<br />

Aufgrund der verschiedenarten Parameter kann der Compiler, trotz Gleichheit der<br />

Bezeichner, die richtige Methode zuordnen.<br />

Noch etwas ist an der Angabe der Intellisens<br />

interessant: Die Klasse Ameise hat die<br />

Nimm()-Methode offensichtlich von der Basisklasse Insekt geerbt. In der Klasse<br />

MeinePowerAmeise wird die Methode jetzt überschrieben. Woran erkennt man das?<br />

Zurück zur Simulation.<br />

Eine Ameise am Zucker soll den Zucker aufnehmen (Nimm(zucker)) und sich dann in<br />

Richtun Bau aufmachen:<br />

public override void ZielErreicht(Zucker zucker)<br />

{<br />

Nimm(zucker);<br />

GeheZuBau();<br />

}<br />

Aufgabe 2<br />

Testen Sie nun die Änderungen.<br />

Erfreulich, dass das<br />

Nahrungskonto durch den<br />

gesammelten Zucker wächst, und<br />

somit das Punktekonto<br />

wenigstens bescheiden wächst.<br />

Verbessern Sie die Fähigkeit<br />

22


Ihres Volkes, indem Sie nun auch Obst einsammeln und in den Bau bringen lassen.<br />

Wie könnte man den Obsttransport verbessern? Wenn schon genügend <strong>Ameisen</strong> für<br />

den Transport abgestellt sind, dann bringt eine weitere Helferin keinen Vorteil!<br />

(Eine mögliche Lösung am Ende des Kapitels!)<br />

Den Zugriff auf die Dateien Insekt.cs oder Ameise.cs hat man nur in der Profiversion<br />

des Programms (Tauschverzeichnis). Hier kann man auf alle Klassen zugreifen und<br />

das Spiel in jeder gewünschten Weise verändern. Da wir noch keinen Profi-Status<br />

erreicht haben, schauen wir uns nur kurz einen kleinen Ausschnitt der Insekt.cs<br />

Datei an:<br />

public void GeheGeradeaus()<br />

{<br />

if (!NimmBefehleEntgegen)<br />

return;<br />

restStreckeI = int.MaxValue;<br />

}<br />

public void GeheGeradeaus(int entfernung)<br />

{<br />

if (!NimmBefehleEntgegen)<br />

return;<br />

restStreckeI = entfernung * Spiel.SPIELFELD_EINHEIT;<br />

}<br />

Man erkennt, dass es sich um die Definition der (überladenen) Methode<br />

GeheGeradeaus() handelt. Wie Sie durch die IntelliSens vielleicht schon entdeckt<br />

haben, kann man die Methode auch mit einem Parameter aufrufen, also etwa:<br />

GeheGeradeaus(schritte). Wichtig ist dabei, dass die Variable schritte vom Typ her<br />

passt, - also eine Integervariable darstellt.<br />

Nehmen wir an, Sie haben irgendwo im Progamm festgelegt, dass schritte = 100 sein<br />

soll. Dann wird beim Aufruf der Methode GeheGeradeaus(schritte) eine Kopie der<br />

Variablen schritte in die lokale Variable entfernung geschrieben. Selbst wenn die<br />

Methode eine Änderung von entfernung vornehmen sollte, - auf die Variable schritte<br />

hat dies keinen Einfluss.<br />

<strong>Von</strong> außen kann man die lokale Variable entfernung nie direkt verändern, da die<br />

Klasse Insekt, wie alle Klassen ihre Methoden kapselt. In der AntMe!-Einsteiger<br />

Fassung haben die Programmier ganze Arbeit gemacht: Man kann die Basisklassen<br />

nicht einmal anschauen! Und das ist der Normalfall: Man muss wissen was die<br />

Methode tut, es ist aber nicht nötig, zu wissen, wie sie es tut!<br />

Obwohl die von uns erstellte Klasse MeinePowerAmeise ja von der Klasse Insekt<br />

abgeleitet ist, kann man von dort aus die Variable entferung ebenfalls ausschließlich<br />

durch Aufruf der public-Methode GeheGeradeaus() beeinflussen.<br />

Bisher haben wir die bösen Käfer außer Acht gelassen. Gut, dass sie langsamer als<br />

die <strong>Ameisen</strong> laufen, sofern letztere keine Last zu tragen haben. Trotzdem ist der<br />

Verlust an <strong>Ameisen</strong> nicht unerheblich, denn sie reagieren bisher überhaupt nicht,<br />

wenn sie einen Käfer in Sichweite haben.<br />

Wenn man die Befehlsliste ansieht, so wird klar, dass, wie im richtigen Leben auch,<br />

Flucht oder Angriff zur Wahl stehen. Ein Angriff ist aber nur sinnvoll, wenn<br />

mindestens 4 andere <strong>Ameisen</strong> bei der Überwältigung des Käfers mithelfen. Ein toter<br />

Käfer ist durchaus lukrativ: Er bringt 150 Punkte für die Power<strong>Ameisen</strong>!<br />

23


Aufgabe 3<br />

Schreiben Sie sinnvolle Anweisungen für die Methode Sieht(Käfer käfer).<br />

Verwenden Sie: AktuelleLast, AnzahlInSichtweite, GreifeAn(käfer),<br />

LasseNahrungFallen(), GeheWeg<strong>Von</strong>(käfer). (Mögliche Lösung am Kapitelende)<br />

Variable, Felder und Eigenschaften<br />

An dieser Stelle müssen wir klären, was der Unterschied zwischen Variablen,<br />

Feldern und Eigenschaften ist.<br />

• Variable-Felder:<br />

Beide sind im RAM repräsentiert, - allerdings dürfen Felder grundsätzlich nur<br />

außerhalb von Methoden definiert werden. Sie sind in der ganzen Klasse<br />

verwendbar.<br />

Man kann bei der Definition von Feldern durch Zugriffsspezifizierer, wie public<br />

oder privat, regeln, ob sie außerhalb der Klasse sichtbar sind oder nicht.<br />

Felder werden im Gegensatz zu Variablen automatisch initialisiert.<br />

Numerische Felder bekommen dabei eine 0, Strings eine leere Zeichenkette ““<br />

• Felder-Eigenschaften:<br />

Wer lange genug für .NET programmiert hat, vergisst möglicherweise, dass es<br />

einen Unterschied zwischen den beiden Begriffen gibt. Und das kommt nicht<br />

von ungefähr. Fragt man beispielsweise durch<br />

aL = PowerAmeise.AktuelleLast<br />

einen aktuellen „Zustand“ unserer Ameise ab, so liegt einem auch der Begriff<br />

„Eigenschaft“ der Ameise auf der Zunge. Die Tatsache aber, dass<br />

AktuelleLast groß geschrieben ist, deutet darauf hin, dass AktuelleLast eine<br />

Methode und kein Feld ist. Und eben diese spezielle Methode wird<br />

Eigenschaft genannt. Verwirrend? Nicht, wenn Sie sich den folgenden<br />

Abschnitt ansehen!<br />

Was ist AktuelleLast oder AnzahlInSichtweite? Ein Feld?<br />

Schauen wir doch einfach mal in der Profiversion nach der Datei Insekt.cs.<br />

Dort gibt es aktuelleLast (kleingeschrieben) und AktuelleLast (großgeschrieben):<br />

private int aktuelleLast = 0;<br />

public int AktuelleLast<br />

{<br />

get { return aktuelleLast; }<br />

internal set{ aktuelleLast = value;}<br />

}<br />

Hier wird klar, dass es sich bei aktuelleLast um ein Feld handelt. Die Initialisierung<br />

mit dem Wert 0 ist nur deshalb vonnöten, weil in einem vorhergehenden Aufruf der<br />

Methode AktuelleLast ein von 0 verschiedener Wert gesetzt sein könnte.<br />

24


Wozu dieser Umstand?<br />

Das Feld aktuelleLast ist private und kann und soll daher nur in dieser Klasse<br />

angesprochen werden (Kapselung!). Um dieses Feld von außen lesen zu können,<br />

gibt es die public Methode AktuelleLast und in dieser die get-Methode. Die<br />

Namensgleichheit ist Absicht. Durch die dann mögliche Schreibweise<br />

Hermine.AktuelleLast (hermine ist eine Powerameise) hat man den Eindruck, das<br />

Feld direkt auszulesen, was ja, wie Sie wissen, wegen des private-Charakters von<br />

aktuelleLast nicht möglich ist.<br />

Zum Schreiben bietet die Methode AktuelleLast die set-Methode an. Durch den<br />

Zugriffsqualifizierer internal aber kann von außen aktuelleLast nicht verändert<br />

werden. (In Wirklichkeit ist die set-Methode in Insekt.cs noch komplizierter. Dies ist<br />

aber für das Verständins nicht weiter von Bedeutung).<br />

Halten wir fest: Obwohl die Methode AktuelleLast public ist, kann das Feld<br />

aktuelleLast von außen lediglich gelesen, nicht aber gesetzt werden.<br />

Aufgabe 4<br />

Suchen Sie in der Datei Insekt.cs nach weiteren Methoden/Eigenschaft-Paaren.<br />

(Beispiel: anzahlInSichtweite und AnzahlInSichtweite).<br />

Wir haben oben die Sieht(Käfer käfer)-Methode implementiert. Weil die <strong>Ameisen</strong><br />

alleine gegen die Käfer hoffnungslos unterlegen sind, ließen wir einen Angriff nur zu,<br />

wenn mehr als 5 andere <strong>Ameisen</strong> in Sichtweite sind.<br />

Wer genau hinschaut stellt fest, dass trotz dieser Bedingung nicht alle <strong>Ameisen</strong>, die<br />

in der Nähe des Käfers sind, angreifen. Einige machen sich aus dem Staub und<br />

überlassen die anderen angreifenden <strong>Ameisen</strong> ihrem Schicksal.<br />

Woran liegt das? Ganz einfach: Nicht alle <strong>Ameisen</strong>, welche Ameise Helga sieht,<br />

werden auch von den anderen gesehen. Wenn Ameise Hubert ziemlich weit rechts<br />

von Helga steht, Ameise Gundi aber recht weit links von ihr, dann sieht Hubert<br />

Gundi vielleicht nicht und ergreift die Flucht.<br />

Markierungen: Call by reference oder Call by value?<br />

Spätestens an dieser Stelle sollten wir eine Möglichkeit (Methode) unserer <strong>Ameisen</strong><br />

ins Auge fassen, die hier sehr nützlich sein kann: SprüheMarkierung(). Eine<br />

durchaus realistische Methode, denn in Wirklichkeit benutzen die <strong>Ameisen</strong><br />

tatsächlich Duftstoffe um anderen aus ihrem Stamm den richtigen Weg zu weisen.<br />

Aus der Befehlsliste entnimmt man, dass die Methode zwei Parameter hat. Der erste<br />

Parameter (Int32, also von -<strong>2.</strong>147.483.648 bis <strong>2.</strong>147.483.647) dient der Information<br />

anderer <strong>Ameisen</strong> und kann über markierung.Information ausgelesen werden. Dies<br />

ist zwar ein sehr großer Bereich, - allerdings bräuchte man, sofern man mit<br />

kartesichen Koordinaten rechnet, zwei Paramter allein für die Information, wo genau<br />

sich ein bestimmtes Objekt befindet. Wir müssen die zwei Informationen auf<br />

25


geschickte Art in die eine Zahl stecken. Wie genau das funktionieren soll, werden wir<br />

uns später ansehen.<br />

Der zweite Parameter bestimmt die Reichweite der Duftmarke. Je weiter desto<br />

kurzlebiger!<br />

Welche Orientierung haben denn die <strong>Ameisen</strong> (ohne unsere Hilfe) mitbekommen?<br />

Die Koordinaten-Klasse hat, entnimmt man der Befehlsliste, zwei Methoden:<br />

Koordinate.BestimmeRichung() und Koordinate.BestimmeEntfernung().<br />

Zu Koordinate.BestimmeRichung():<br />

Schaut eine Ameise genau nach Osten und sieht dort beispielsweise einen<br />

Zuckerberg, dann würde sich der Wert 0 (Grad) ergeben. Im Uhrzeigersinn geht es<br />

dann weiter: Süden: 90 (Grad), Westen: 180 (Grad), Norden: 270 (Grad).<br />

Zu Koordinate.BestimmeEntfernung():<br />

Die Längeneinheit ist „ein Schritt“. In einer Spielrunde bewegt sich eine Ameise vier<br />

Schritte weit. Das Spielfeld selbst hat das Maß 1200 x 900 Schritte.<br />

„Sieht“ eine Ameise ein Stück Obst, so kann sie über die beiden genannten<br />

Methoden lediglich die Richtung, die sie selbst einnehmen muss und ihre eigene<br />

Entfernung auslesen. Leider sind das nur „relative“ Daten, - also Daten, die in dieser<br />

Form anderen <strong>Ameisen</strong> nicht viel weiterhelfen.<br />

Man muss offensichtlich dafür sorgen, dass aus den relativen Daten absolute Daten<br />

werden. Stichwort: Kartesische Koordinaten! Und diese gilt es in einer Int32-Zahl auf<br />

eindeutige Art unterzubringen.<br />

Bevor wir uns an die Arbeit machen, ist leider noch etwas Theorie unumgänglich.<br />

Wenn eine Ameise Zucker von einem Haufen wegnimmt, so muss das Feld menge,<br />

durch die Eigenschaft Menge(Zucker zucker1) verändert werden, denn der Vorrat<br />

nimmt ja ab.<br />

Kommt eine weitere Ameise (theoretisch könnte es auch ein Zucker-fressender Käfer<br />

sein) zu diesem Objekt, so muss er die vorhin veränderte menge vorfinden. Dies wird<br />

dadurch realisiert, dass die Methode Menge als Parameter das (von der Klasse<br />

Zucker abgeleitete) Objekt zucker1 geliefert bekommt. In diesem Fall spricht man<br />

von einem call-by-reference:<br />

Anstatt die Daten zu kopieren werden Referenzen auf die Daten übergeben. Man<br />

kann sich das als Adress-Zeiger vorstellen: Beim Aufrufer zeigt eine Variable auf ein<br />

bestimmtes Datum. An anderer Stelle zeigt eine nächste Variable auf dasselbe<br />

Datum.<br />

Methodenaufrufe an einem so übergebenen Objekt arbeiten also auf demselben<br />

Objekt, das auch außerhalb sichtbar ist. Objekte landen speichertechnisch auf dem<br />

bevorzugten Heap des RAM. Bevorzugt deshalb, weil im Gegensatz zum Stack die<br />

Speicherfreiverwaltung von .NET für die notwendige „Buchhaltung“ und Lebensdauer<br />

sorgt.<br />

Den Stack hingegen arbeitet der Prozessor von „oben nach unten“ ab. Dort werden<br />

auch Werte abgelegt, die bei einem Aufruf lediglich kopiert werden. Einen derartigen<br />

Aufruf nennt man call-by-value:<br />

Bei der Methode Sieht(Käfer käfer) beispielsweise gibt es das Feld<br />

26


anzahlInSichweite, das durch die gleichnamige Methode ausgelesen wird:<br />

AnzahlInSichtweite(). Sehen Sie sich folgenden Ausschnitt eines Programmcodes<br />

an:<br />

Metthode AnzahlInSichtweite aus Basisklasse Insekt:<br />

public int AnzahlInSichtweite<br />

{get { return anzahlInSichtweite; } }<br />

Aufruf außerhalb:<br />

int feinde = roxane.AnzahlInSichtweite; // 1.Zeile ; roxane ist eine Powerameise<br />

feinde = 2*feinde; // <strong>2.</strong>Zeile<br />

Durch Zeile 1 bekommt das Feld feinde den gleichen Wert (durch kopieren), wie<br />

anzahlInSichweite.<br />

In Zeile 2 wird die Anzahl der feinde verdoppelt, - anzahlInSichweite bleibt davon<br />

aber unbeindruckt!<br />

Domit bekommt der Fachausdruck Werteparameter für int, double, Int64 etc. einen<br />

Sinn: Ihr Aufruf ist ein call-by-value und ihr Platz ist im Stack!<br />

Struct und static<br />

Mit diesem Wissen ausgerüstet schaut man verunsichert auf die Methode<br />

Koordinate.BestimmeEntfernung(). Aus den bisherigen Erfahrungen halten wir<br />

Koordinate für ein Objekt und BestimmeEntfernung für eine Methoden seiner<br />

Klasse. Glauben Sie wirklich, dass es sinnvoll ist, Koordinaten mehrfach zu<br />

instantiieren? Es gibt duch nur eine Sorte Koordinaten, wenn man sich einmal<br />

festgelegt hat.<br />

Gut, - dann ist Koordinate also eine Klasse. Aber man kann Methoden der Klasse<br />

doch nur über Objekte aufrufen?!<br />

Schauen wir in der Profi-Version nach:<br />

public struct Koordinate<br />

{<br />

…<br />

public static int BestimmeEntfernung(IKoordinate o1, IKoordinate o2)<br />

{<br />

return BestimmeEntfernungI(o1.Koordinate, o<strong>2.</strong>Koordinate)<br />

/ Spiel.SPIELFELD_EINHEIT;<br />

}<br />

…<br />

}<br />

Koordinate ist tatsächlich keine Klasse (class) sondern eine Struktur (struct)!<br />

Der Hauptunterschied zwischen class und struct: Ist ein Parameter eine Struktur, so<br />

ist der Aufruf ein call-by-value! Na, also! Und damit „arbeiten“ nicht alle <strong>Ameisen</strong> an<br />

einem einzigen Feld herum. Entfernung zu einem anderen Objekt ist doch eine<br />

27


individuelle Größe und sollte daher auch einen eigenen Speicher für jede Ameise<br />

bekommen.<br />

Der Zusatz static vor der Methode BestimmeEntferung besagt, dass es sich hier um<br />

eine Klassenmethode handelt, die keine Realisierung in einem Objekt haben kann.<br />

Dadurch kann man direkt Koordinate.BestimmeRichtung() aufrufen, ohne durch<br />

new ein Objekt instantiiert zu haben. Man verwendet static-Methoden immer dann,<br />

wenn es um globale Aufgaben geht. Und in der Tat ist es ja vollkommen egal, um<br />

welche Objekte o1 und o2 es sich handelt. Es kommt nur darauf an wo sie stehen.<br />

Natürlich arbeitet die struct Koordinate intern auch mit kartesischen Koordinaten, um<br />

etwa die Entfernung zwischen zwei Objekten zu bestimmen: k1.x , k1.y und k<strong>2.</strong>x ,<br />

k2y werden sie hier genannt:<br />

internal static int BestimmeEntfernungI(Koordinate k1, Koordinate k2)<br />

{<br />

double deltaX = k1.x - k<strong>2.</strong>x;<br />

double deltaY = k1.y - k<strong>2.</strong>y;<br />

int entfernung =<br />

(int)Math.Round(Math.Sqrt(deltaX * deltaX + deltaY * deltaY));<br />

entfernung = entfernung - k1.radius - k<strong>2.</strong>radius;<br />

if (entfernung < 0)<br />

return 0;<br />

return entfernung;<br />

}<br />

Da sie aber alle ausschließlich unter internal-Methoden zu finden sind, gibt es in der<br />

Einsteigerfassung von AntMe! keine Möglichkeit an sie ranzukommen.<br />

Müssen wir also selbst Hand anlegen!<br />

Zuvor aber der Versuch, mit dem auszukommen, was da ist:<br />

Koordinate.BestimmeEnfernung() und Koordinate.BestimmeRichtung():<br />

Den Methoden müssen zwei Paramter übergeben werden: Objekt 1 und Objekt <strong>2.</strong><br />

Versuchen wir es zunächst mit Koordinate.BestimmeRichtung().<br />

Objekt 1 ist die Ameise. Als ausrufende wird sie schlicht this genannt.<br />

Diese Richtungs-Information kann man in der Methode SprüheMarkierung()<br />

übermitteln.<br />

Im folgenden Versuch die Erzeugung einer Markierung für den Fall, dass die Ameise<br />

Obst sieht:<br />

public override void Sieht(Obst obst)<br />

{<br />

SprüheMarkierung(Koordinate.BestimmeRichtung(this, obst), 80);<br />

if (AktuelleLast == 0 && BrauchtNochTräger(obst))<br />

GeheZuZiel(obst);<br />

}<br />

Reaktion auf die Wahrnehmung der Markierung:<br />

28


public override void Sieht(Markierung markierung)<br />

{<br />

if (AktuelleLast==0)<br />

{<br />

DreheInRichtung(markierung.Information);<br />

GeheGeradeaus(50);<br />

}<br />

}<br />

Am Radius 80 der Markierung und an der Zahl der Schritte in die (vermeintliche)<br />

Zielrichtung, 50, kann noch gefeilt werden.<br />

Die Begeisterung, mit der unsere <strong>Ameisen</strong> nun das Fallobst in den Bau bringen, ist<br />

deutlich zu erkennen. Leider kommen die Käfer auf diese Weise fast ungeschoren<br />

davon.<br />

Außerdem gehen die von einer Markierung beeinflussten <strong>Ameisen</strong> weg vom Obst,<br />

wenn sie auf der „falschen“ Seite stehen.<br />

Werden die <strong>Ameisen</strong> von zwei verschiedenen Markierungen erreicht, bleiben sie<br />

einfach stehen.<br />

Trotzdem: Mit den obigen Werten kommt man, mit einigem Glück, auf über 5000<br />

Punkte!<br />

Das muss doch besser gehen!<br />

Zunächst sollten unsere <strong>Ameisen</strong> auch bei der Sichtung eines Zuckerberges eine<br />

Markierung vornehmen. Je näher sie an das Ziel kommt, desto kleiner (und damit<br />

langlebiger) sollten die Radien der Markierung sein. Wenn man den Radius<br />

gleichgroß, wie die Entfernung wählt, dann gibt es keine Markierung auf der anderen<br />

Seite. So kann man vielleicht das Weglaufen einschränken:<br />

public override void Sieht(Zucker zucker)<br />

{<br />

if (Koordinate.BestimmeEntfernung(this,zucker)


Auch in Sieht(Markierung markierung) kann man noch optimieren: Wenn die Ameise<br />

schon ein Ziel hat, dann soll sie sich nicht beirren lassen:<br />

if (AktuelleLast==0 && Ziel==null)<br />

Versuchen Sie es: 6000 Punkte sollten drin sein!<br />

Jetzt wollen wir die <strong>Ameisen</strong> auch Markierungen sprühen lassen, wenn sie außerhalb<br />

der Sichtweite des Zuckers ihre wertvolle Last in den Bau tragen:<br />

public override void Tick()<br />

{<br />

if (AktuelleLast>0 && Ziel is Bau)<br />

{<br />

SprüheMarkierung(Koordinate.BestimmeRichtung(Ziel, this), 20);<br />

}<br />

}<br />

Das kann man schon fast eine <strong>Ameisen</strong>straße nennen! Ergebnis:<br />

Natürlich spielt auch der Zufall eine Rolle. Daher können es durchaus<br />

auch mal 6500 Punkte werden…<br />

Die hier erzeugte Spezies werden wir SammlerPlus taufen. (Das Plus,<br />

weil es keine reine Sammler sind. )<br />

Typen<br />

Vermutlich ist Ihnen schon aufgefallen,<br />

dass im Tab „lokales Spiel“ auch eine<br />

andere <strong>Ameisen</strong>-Spezies ausgesucht<br />

werden kann.<br />

Neben einer möglicherweise anderen<br />

Strategie wurde hier auch der Typ der<br />

Ameise verändert. Das können Sie auch!<br />

Es gibt sieben Felder:<br />

Angriff, Energie, Last, etc.<br />

Im unveränderten Zustand sind alle auf 0<br />

gestellt. Möchte man einem Attribut den<br />

Vorzug geben, so kann man dessen Punktezahl maximal um 2 Punkte erhöhen.<br />

Gleichzeitig müssen aber ebensoviele Punkte an anderer Stelle abgezogen werden.<br />

Wichtig: Man kann als minimalen Wert nur -1 einstellen.<br />

So sieht der Programmcode dann zum Beispiel (Spieler.cs) aus:<br />

30


[Typ(<br />

Name = "Kämpfer",<br />

GeschwindigkeitModifikator = -1,<br />

DrehgeschwindigkeitModifikator = -1,<br />

LastModifikator = -1,<br />

ReichweiteModifikator = -1,<br />

SichtweiteModifikator = 0,<br />

EnergieModifikator = 2,<br />

AngriffModifikator = 2<br />

)]<br />

Aufgabe 5<br />

Verändern Sie den Code in den Methoden und im Typ so,<br />

• dass eine reine Sammlerkolonie entsteht.<br />

• dass eine reine Kämpferkolonie entsteht.<br />

Welche Variante bringt mehr Punkte?<br />

Tipps:<br />

• Wenn Sie einen Typ, beispielsweise „Kämpfer“ erzeugen, dann dürfen Sie<br />

nicht vergessen, in der „Erzeugungsabteilung“ den gewünschten Typ<br />

anzugeben:<br />

public static string BestimmeTyp(Dictionary anzahl)<br />

{<br />

return "Kämpfer";<br />

}<br />

• Je nach Art der Spezies sollte man beim Tick-Ereignis Optimierungen<br />

vornehmen. Für die Kämpfer etwa, die ja von alleine nicht zum Bau zurückgehen,<br />

muss festgelegt werden, dass sie im richtigen Moment den<br />

Heimweg antreten, um Energie aufzutanken:<br />

public override void Tick()<br />

{<br />

if (Reichweite - ZurückgelegteStrecke - 100 < EntfernungZuBau)<br />

GeheZuBau();<br />

if (AktuelleEnergie < MaximaleEnergie * 2 / 3)<br />

GeheZuBau()<br />

}<br />

• Weitere Anregungen erhalten Sie, wenn Sie die Demospieler-cs-Dateien<br />

studieren. (Tauschverzeichnis)<br />

31


Man kann auch zwei oder<br />

mehr Typen von <strong>Ameisen</strong><br />

erzeugen. Mit dieser äußerst<br />

aggressiven Art (30 Arbeiter,<br />

70 Kämpfer) kommt man<br />

den 7000 Punkten schon<br />

recht nah: 6762 Punkte!<br />

Versuchen Sie noch weiter<br />

zu spezialisieren, indem Sie<br />

beispielsweise auf Obst bzw<br />

auf Zucker optimierte<br />

<strong>Ameisen</strong> schaffen.<br />

Quelltext (in Auszügen) zum<br />

<strong>Ameisen</strong>-Stamm rechts:<br />

[Typ(<br />

)]<br />

Name = "Arbeiter",<br />

GeschwindigkeitModifikator = 2,<br />

DrehgeschwindigkeitModifikator = -1,<br />

LastModifikator = 1,<br />

ReichweiteModifikator = 0,<br />

SichtweiteModifikator = 0,<br />

EnergieModifikator = -1,<br />

AngriffModifikator = -1<br />

[Typ(<br />

Name = "Kämpfer",<br />

GeschwindigkeitModifikator = -1,<br />

DrehgeschwindigkeitModifikator = -1,<br />

LastModifikator = -1,<br />

ReichweiteModifikator = -1,<br />

SichtweiteModifikator = 0,<br />

EnergieModifikator = 2,<br />

AngriffModifikator = 2<br />

)]<br />

public static string BestimmeTyp(Dictionary anzahl)<br />

{<br />

if (anzahl["Arbeiter"] < 40)<br />

return "Arbeiter";<br />

else return "Kämpfer";<br />

}<br />

public MeinePowerAmeise(Volk volk, int typIndex) : base(volk, typIndex) {<br />

}<br />

32


public override void Wartet()<br />

{<br />

GeheGeradeaus(50);<br />

DreheUmWinkel(Zufall.Zahl(-6, 6));<br />

}<br />

public override void WirdMüde() {<br />

}<br />

public override void Sieht(Zucker zucker)<br />

{<br />

if (Koordinate.BestimmeEntfernung(this,zucker) 0) LasseNahrungFallen();<br />

SprüheMarkierung(-1, 200);//Nur die Kämpfer-<strong>Ameisen</strong> sollen reagieren<br />

if (AnzahlInSichtweite > 5)<br />

{<br />

GreifeAn(käfer);<br />

}<br />

else GeheWeg<strong>Von</strong>(käfer, 80);<br />

}<br />

public override void Sieht(Markierung markierung)<br />

{<br />

if (AktuelleLast==0 && Ziel==null)<br />

{<br />

if (markierung.Information == -1 && Typ == "Kämpfer")<br />

{<br />

33


}<br />

}<br />

}<br />

}<br />

GeheZuZiel(markierung);<br />

}<br />

if (markierung.Information != -1 && Typ == "Arbeiter")<br />

{<br />

DreheInRichtung(markierung.Information);<br />

GeheGeradeaus(50);<br />

}<br />

public override void WirdAngegriffen(Käfer käfer)<br />

{<br />

if (Typ=="Kämpfer")<br />

GreifeAn(käfer);<br />

else GeheWeg<strong>Von</strong>(käfer, 80);<br />

}<br />

public override void ZielErreicht(Zucker zucker)<br />

{<br />

Nimm(zucker);<br />

GeheZuBau();<br />

}<br />

public override void ZielErreicht(Obst obst)<br />

{<br />

Nimm(obst);<br />

GeheZuBau();<br />

}<br />

public override void Tick()<br />

{<br />

if (AktuelleLast > 0 && Ziel is Bau && Typ == "Arbeiter")<br />

{<br />

SprüheMarkierung(Koordinate.BestimmeRichtung(Ziel, this), 20);<br />

}<br />

if (Typ=="Kämpfer")<br />

{<br />

if (Reichweite - ZurückgelegteStrecke - 100 < EntfernungZuBau)<br />

GeheZuBau();<br />

if (AktuelleEnergie < MaximaleEnergie * 2 / 3)<br />

GeheZuBau();<br />

}<br />

}<br />

Möglicherweise kommen Einige durch ausgeklügelte Kombinationen von <strong>Ameisen</strong>-<br />

Spezialisten und einer noch geschicktere Strategie auf 7000 - 7500 Punkte. Man<br />

merkt jedoch, dass für ein wirklicher Fortschritt neue Ideen gefragt sind. Zu denen<br />

kommen wir jetzt!<br />

34


Lokales Gedächtnis<br />

Praktisch wäre, wenn sich die Arbeiter-<strong>Ameisen</strong> einen einmal gefundenen<br />

Zuckerberg merken könnten.<br />

Ob Insekten tatsächlich ein Gedächtnis haben, können Sie übrigens hier nachlesen:<br />

http://www.br-online.de/wissen/wespen-insekten-gedaechtnis-ID1222248824405.xml<br />

Eine derartige Speicherfunktion zu integrieren, ist mit Hilfe der OOP ein Kinderspiel!<br />

Hier eine Möglichkeit (Code in Auschnitten):<br />

public class MeinePowerAmeise : Ameise {<br />

//Gedächtnis Speicher<br />

private Zucker zumerkenderZucker;<br />

.………………………………………………………………………………………………<br />

public override void Sieht(Zucker zucker)<br />

{<br />

.…………………………………………………………………………………..<br />

if (AktuelleLast == 0 && Typ == "Arbeiter")<br />

{<br />

zumerkenderZucker = zucker;//Hier wird der Zucker gespeichert<br />

GeheZuZiel(zucker);<br />

}<br />

}<br />

.…………………………………………………………………………<br />

public override void Tick()<br />

{<br />

.……………………………………………………………………………………<br />

//Wenn ein Zuckerberg leer ist, muss er aus dem Gedächtnis gelöscht werden<br />

if (AktuelleLast==0 && zumerkenderZucker != null && Typ == "Arbeiter")<br />

{<br />

if (zumerkenderZucker.Menge


Um vergleichbare Ergebnisse zu bekommen, gilt in der AntMe!-Community ein<br />

ehernes Gesetzt: Die public-Methoden und Felder, die man in der Profi-Version ja<br />

verändern könnte, sind Tabu! Mehr noch: Es gilt als unfair, wenn man in diesen<br />

class-Dateien Änderungen vornimmt. Das trifft sich gut, denn genau dies ist auch das<br />

eiserne Gesetz der OOP: Was gekapselt ist, geht einen nichts an!<br />

Allerdings gibt es selbst unter strengster Auslegung dieser Gesetze durchaus<br />

Möglichkeiten, die <strong>Ameisen</strong> deutlich klüger zu machen, als es die Entwickler anfangs<br />

vorgesehen hatten. Das Stichwort dazu heißt static.<br />

Globales Gedächtnis und ArrayList<br />

Im letzten Programmentwurf hatte jede einzelne Ameise ein Gedächtnis. In der<br />

Sprache der Objektorientierung: Die Daten, die im Heap für die einzelnen Objekte<br />

gespeichert sind, könnnen von den anderen Objekten (<strong>Ameisen</strong>) nicht gelesen<br />

werden.<br />

Weiter oben haben Sie bereits eine static-Methoden verwendet:<br />

public static int BestimmeEntfernung(IKoordinate o1, IKoordinate o2)<br />

Hier ging es darum, dass die Methode BestimmeEnfernung nicht etwa auf eine<br />

Adresse im Heap zeigen soll, sondern dass der entsprechende Wert (value) im Stack<br />

abgelegt wird. Obwohl prinzipiell möglich, wird dieser Wert nach seiner Nutzung nicht<br />

weiter gespeichert. Der Garbage-Collector sorgt für eine schnelle Entsorgung.<br />

Was wäre aber, wenn man die von einem <strong>Ameisen</strong>-Objekt gefundenen Orte für Käfer<br />

oder Obst in einem public-static-Feld abspeichert? Dann können alle anderen<br />

<strong>Ameisen</strong> die Daten dort lesen und gegebenenfalls auch korrigieren! Auf die Realität<br />

übertragen würde dies bedeuten, dass die <strong>Ameisen</strong> über ein sofort verfügbares<br />

globales Gedächtnis verfügen, etwa so wie ein großer Teil Menschheit durch das<br />

Internet. Biologisch sinnvoll oder nicht (man kann sich ja vorstellen, dass die<br />

<strong>Ameisen</strong> auch über gewisse Distanzen kommunizieren können), - wir werden diese<br />

Technik nutzen. Unsere Einschränkung, dass Gedächtnis nur maximal 500 Spieltakte<br />

bestehen zu lassen, ist nicht ganz uneigennützig: Käfer und Obst bleibt ja nicht an<br />

einem Ort stehen. So würde das globale Gedächtnis, dass von allen <strong>Ameisen</strong><br />

gefüttert wird, sehr schnell so groß, dass seine Verwaltung auf dem RAM auch<br />

schnelle Rechner an den Rande der Leistungsfähigkeit bringen würde.<br />

In einem ersten Versuch soll eine „Killerameisen“-Population erschaffen werden: Die<br />

<strong>Ameisen</strong> konzentrieren sich ausschließlich aufs Töten von Käfern. Ein toter Käfer<br />

bringt 150 Punkte. Daher ist mit der globalen Gedächtnis-Methode ein respektables<br />

Ergebnis zu erwarten. Als Speicher soll uns eine ArrayList dienen.<br />

Interface<br />

In der .NET -Hilfe bekommt man zu ArrayList die Information, dass es sich dabei um<br />

eine Klasse handelt, die das IList-Interface unter Verwendung eines dynamischen<br />

Arrays implementiert. Alles klar?!? Vermutlich nicht. Daher zunächst die Frage:<br />

36


Was ist ein Interface (auch Schnittstelle genannt)?<br />

Am besten stellen Sie sich ein Interface als eine Klasse ohne Felder und ohne<br />

Implementierungen vor. Vermutlich denken Sie dabei an die abstrakten Klassen<br />

(1.Kapitel). Das dies nicht korrekt ist, wird Ihnen klar, wenn Sie sich daran erinnern,<br />

dass abstrakte Klassen sehr wohl Implementierungen von Eigenschaften und<br />

Methoden enthalten können. Weitere spezifischen Eigenheiten eines Interface (im<br />

Vergleich mit abstrakten Klassen):<br />

• In Interfaces sind immer alle Eigenschaften und Methoden abstract und<br />

public. In abstrakten Klassen kann es beispielsweise auch private geben.<br />

• In Interfaces kann es, wie schon gesagt, keine Felder geben. In<br />

abstrakten Klassen ist dies nicht der Fall.<br />

• Damit eine Klasse von einer abstrakten Klasse erben kann, muss sie die<br />

abstrakte Klasse irgendwo als Vorfahr haben (also davon abgeleitet sein).<br />

Im Gegensatz dazu kann man viele verschiedene Interfaces in einer<br />

Klasse impelementieren.<br />

Ein Beispiel zum letzten Punkt:<br />

In AntMe! gibt es die Klasse Nahrung und die Klasse Insekt. Sie sind nicht<br />

voneinander abgeleitet. So verschieden die beiden Klassen auch sein mögen, -<br />

beide haben das Interface IKoordinate implementiert, weil beide Klassen in den<br />

erzeugten Objekten Orts-Koordinaten benötigen.<br />

Es gibt also bei den Interfaces Eigenschaften und Methoden, - diesen fehlt aber der<br />

Anweisungsblock, bzw. der set- und get-Teil der Eigenschaften. Der Zweck eines<br />

Interface besteht offensichtlich darin, dass es von mehreren nicht voneinander<br />

abgeleiteten Klassen implementiert wird. Betrachten wir als Beispiel das Interface<br />

IKoordinate aus AntMe!<br />

public interface IKoordinate ///Deklaration des Interface<br />

{<br />

/// Liest die Koordinate eines Objekts auf dem Spielfeld aus.<br />

Koordinate Koordinate { get; }<br />

}<br />

public struct Koordinate ///Hier beginnt die Klassendefinition<br />

{<br />

private int radius;<br />

private int richtung;<br />

private int x;<br />

private int y;<br />

.……………....………………………………………………………………….<br />

internal Koordinate(int x, int y, int radius, int richtung)<br />

{<br />

this.x = x * Spiel.SPIELFELD_EINHEIT;<br />

this.y = y * Spiel.SPIELFELD_EINHEIT;<br />

37


In diesem Konstruktor werden alle Werte der Struktur initialisiert<br />

// im Anschluß gleich wieder überschrieben:<br />

this.radius = 0;<br />

this.richtung = 0;<br />

Radius = radius * Spiel.SPIELFELD_EINHEIT;<br />

Richtung = richtung;<br />

}<br />

.…………………………………………………………………………………….<br />

Das war die Deklaration. Wie wird IKoordinate implementiert? Hier am Beispiel der<br />

Klasse Nahrung Nahrung (also den Zucker und das Obst). Sie verwendet<br />

IKoordinate. Ein Auszug aus der Klasse:<br />

:<br />

public abstract class Nahrung : IKoordinate ///Implementierung des Interface<br />

{<br />

/// Die Position der Nahrung auf dem Spielfeld.<br />

protected Koordinate koordinate;<br />

}<br />

/// Eigenschaft dazu:<br />

public Koordinate Koordinate<br />

{<br />

get { return koordinate; }<br />

internal set { koordinate = value; }<br />

}<br />

// Zuckerhaufen<br />

public sealed class Zucker : Nahrung<br />

{<br />

/// Zucker Konstruktor.<br />

internal Zucker(Koordinate koordinate, int menge)<br />

: base(koordinate, menge) {}<br />

.}………………………………………………………………………………………..<br />

(Der Zugriffsmodifizierer sealed bedeutet, dass von dieser Klasse nicht geerbt<br />

werden kann).<br />

Halten wir fest: Die Schnittstellen werden durch das Schlüsselwort interface<br />

implementiert, gefolgt vom Namen der Schnittstelle, der mit einem I beginnen sollte.<br />

Wie Sie sehen können, muss die implementierende Klasse (z.B. Nahrung) selbst für<br />

die Definionen der Methoden und Eigenschaften sorgen.<br />

Der Vorteil: Wenn man weiß, dass eine Klasse ein bestimmtes Interface<br />

implementiert, dann weiß man sofort, dass sie die Methoden und Eigenschaften aus<br />

dem Interface zur Verfügung stellt!<br />

38


An die Koordinaten der Nahrung kommt man nicht direkt. (Sie sind private definiert).<br />

Nahrung wie Zucker oder Obst haben aber das Interface IKoordinate implementiert,<br />

also kann man zumindest eine Referenz auf die Ortskoordinaten bekommen. Man<br />

muss, wie Sie bald sehen werden, die absoluten Zahlen für den Ort überhaupt nicht<br />

kennen. Man muss nicht einmal wissen, ob kartesische oder Polarkoordinaten<br />

verwendet werden.<br />

Jetzt verstehen Sie den weiter oben schon zitierten Programm-Code aus der Klasse<br />

Koordinaten besser:<br />

public static int BestimmeEntfernung(IKoordinate o1, IKoordinate o2)<br />

{<br />

return BestimmeEntfernung(o1.Koordinate, o<strong>2.</strong>Koordinate);<br />

}<br />

Hier wird mittels des Interface IKoordinate die Methode BestimmeEnfernung<br />

implementiert. So kann jede Klasse, die IKoordinate verwendet, die Entfernung<br />

zwischen zwei konkreten Objekten bestimmen. Dabei spielt es keine Rolle, ob die<br />

Objekte o1 und o2 aus der gleichen Klasse stammen. Denkbar wäre zum Beispiel,<br />

dass o1 die Ameise Frida und o2 der Käfer Willibald ist.<br />

Zurück zu unserem Ausgangsproblem:<br />

ArrayList ist also eine Klasse mit dem Interface IList. Damit hat man in der ArrayList<br />

alle Eigenschaften und Methoden von IList, wie beispielsweise die Sort()-Methode.<br />

Ein dynamischer Array ist einfach eine Liste von Daten, deren Länge nicht von<br />

vornherein festgelegt ist.<br />

ArrayList testen<br />

Damit Sie dies alles auch gut nachvollziehen können, programmieren wir nun eine<br />

Oberfläche zur Speicherung, Sortierung, Ausgabe und Löschung von Strings.<br />

So soll sie aussehen:<br />

39


Zunächst erzeugen wir eine Klasse namens ListeArrayList.cs, die man auch<br />

String-Speicher Liste hätte nennen können.<br />

class ListeArrayList<br />

{<br />

private static ArrayList stringListe = new ArrayList();<br />

public static ArrayList StringListe //Eigenschaft<br />

{<br />

get { return stringListe;}<br />

}<br />

public static void StringSpeichern(string zeichenfolge)<br />

{<br />

stringListe.Add(zeichenfolge);<br />

}<br />

public static void StringLoeschen(string zeichenfolge)<br />

{<br />

stringListe.Remove(zeichenfolge);<br />

}<br />

public static string GibString(int nummer)<br />

{<br />

if (nummer


Aufgabe 6<br />

Erstellen Sie die Oberfläche der ArrayList-Studie (siehe oben). Schreiben Sie mit<br />

Hilfe der ListeArrayList für anderen Buttons die Click-Ereignisse.<br />

Testen Sie das Programm ausführlich, so dass Sie in der Lage sind, folgende Fragen<br />

zu beantworten:<br />

• Welchen Index hat das erste Listenelement?<br />

• Wie werden die Strings beim Drücken auf den entsprechenden Button<br />

sortiert?<br />

• Kann man auch leere Strings speichern? Wenn ja, was zeigt dann das<br />

Textfeld an, wenn man den leeren String wieder ausgibt?<br />

• Entspricht ein nicht belegter Speicherplatz einem leeren String?<br />

• Weshalb ist die if-Abfrage in der GibString-Methode von ListeArrayList nötig?<br />

• Wenn beispielsweise der 10-te String in der Liste gelöscht wird, was passiert<br />

dann mit dem vormals 11-ten?<br />

(Eine mögliche Lösung finden Sie am Kapitelende)<br />

Einen wesentlichen Unterschied zur Ortsspeicher-Klasse in AntMe! gibt es hier<br />

allerdings: In der ListeArrayList werden Strings auf den Speicherplätzen abgelegt.<br />

In der Ortsspeicher-Klasse werden wir Käfer-Objekte (und später Obst- und Zucker-<br />

Objekte) platzieren. Das bedeutet, dass auf den jeweiligen Plätzen der ArrayList nur<br />

Referenzen zu den Objekten im Heap gespeichert werden. Zeigt eine Referenz ins<br />

Leere, so bekommt man hier das Ergebnis null zurück.<br />

(Streng genommen werden auch bei den Strings nur Referenzen abgespeichert. Würde man eine<br />

Liste bestehend aus Integer erzeugen, so werden diese dann tatsächlich als Wert in den einzelnen<br />

Listenelementen abgelegt. String: Call by reference, Integer; Call by value)<br />

41


Die Klasse Ortsspeicher<br />

Nun wollen wir endlich zu unseren Killer-<strong>Ameisen</strong> kommen. Wie oben wird zunächst<br />

eine Klasse erzeugt, die das globale Gedächtnis speichern soll. Wir werden sie<br />

Ortsspeicher nennen:<br />

using System.Collections;<br />

namespace AntMe.Spieler<br />

{<br />

class Ortsspeicher<br />

{<br />

private static ArrayList käferOrt = new ArrayList();<br />

} }<br />

private static ArrayList geloeschterOrt = new ArrayList();<br />

public static ArrayList KäferOrt // Eigenschaft<br />

{<br />

get { return käferOrt; }<br />

}<br />

public static int Schleifenzaehler = 0;<br />

public static ArrayList GeloeschterOrt // Eigenschaft<br />

{<br />

get { return geloeschterOrt; }<br />

}<br />

public static void OrtSpeichern(IKoordinate ort) // Eigenschaft<br />

{<br />

if (ort != null)<br />

{<br />

if (! käferOrt.Contains(ort) && ! geloeschterOrt.Contains(ort))<br />

KäferOrt.Add(ort);<br />

}<br />

}<br />

public static void OrtLoeschen(IKoordinate ort) // Eigenschaft<br />

{ käferOrt.Remove(ort); geloeschterOrt.Add(ort);}<br />

public static Käfer LiesKäfer(int i) // Eigenschaft<br />

{<br />

if (käferOrt.Count >= i+1)<br />

{<br />

IKoordinate ort = (IKoordinate) käferOrt[i] ; //unboxing<br />

return (ort as Käfer);<br />

}<br />

return null;<br />

}<br />

42


Einige Kommentare zu diesem Code:<br />

• Wir benötigen zwei ListArrays, kaeferOrt, geloeschterOrt, weil die vernichteten<br />

Käfer in der ersten Liste gestrichen und in der zweiten Liste hinzugenommen<br />

werden. Damit kann eine Ameise, die auf dem Weg zu einem nicht mehr<br />

existierenden Käfer in der geloeschterOrt-Liste feststellen kann, dass sie sich<br />

neuen Aufgaben widmen kann.<br />

• Die public static Integer-Variable Schleifenzaehler soll später zum Zählen der<br />

Spielrunden verwendet werden. (4500 Runden dauert ein Spiel).<br />

• Die Eigenschaft Ortspeichern nimmt eine Variable ort entgegen, die<br />

IKoordinate kennt. Natürlich soll sie nur dann gespeichert werden, wenn sie<br />

weder in der kaeferOrt-Liste bereits steht, noch in der geloeschterOrt-Liste<br />

eingetragen ist. Frage: Woher kennt käferOrt eigentlich Remove() bzw Add() ?<br />

• Die LiesKäfer-Eigenschaft soll im folgenden Theorieteil etwas genauer unter<br />

die Lupe genommen werden.<br />

Boxing und unboxing<br />

Betrachten Sie folgenden Code:<br />

int i = 25; // ein value type<br />

object o = i; // boxing (implizit)<br />

int j = (int)o; // unboxing<br />

In der ersten Zeile wird eine Integerzahl (Typ value)<br />

auf den Stack gelegt. In der zweiten Zeile wird durch<br />

eine Technik, die man boxing nennt, eine Kopie<br />

dieser Zahl unter dem Namen o in den Heap<br />

befördert (Typ reference). Dort fristet das neue Objekt<br />

sein Dasein in stetiger Erwartung auf den Garbage-<br />

Collector. Und dieser kommt besonders schnell, wenn<br />

das Objet o durch unboxing unter dem Namen j in<br />

den Stack gelegt wird. Das Ganze ist auch durch den<br />

Begriff Typenkonvertierung hinreichend erklärt. Das<br />

Bild rechts stammt von der msdn-Hilfeseite:<br />

In der Zeile return (ort as Käfer); findet durch as ebenfalls eine Typenkonvertierung<br />

statt. ort ist ja zunächst nur ein Objekt, das IKoordinate kennt. Durch diese Zeile wird<br />

der Typ festgelegt. (Es gibt übrigens noch <strong>Ameisen</strong>-, Obst- und Zucker-Objekte!)<br />

Zurück zum Spiel. Bisher haben wir lediglich dafür gesorgt, dass alle <strong>Ameisen</strong> auf<br />

das globale Gedächtnis zugreifen können. Sie haben dort Lese- und Schreibrechte.<br />

Alle <strong>Ameisen</strong> zu Kämpfern zu machen, ist sicher nicht günstig, denn man kann die<br />

Eigenschaften der <strong>Ameisen</strong> ja immer nur in bestimmten Bereichen verbessern, um<br />

sie in anderen zu verschlechtern. Was unsere Kampf-<strong>Ameisen</strong> auf jeden Fall<br />

benötigen, ist ein maximaler Angriffsmodifikator (2) und möglichst etwas mehr als<br />

durchschnittliche Energie(1). Ein Vorschlag für die Kämpfer:<br />

43


Kämpferameisen mit globalem Gedächtnis<br />

Name = "Kämpfer",<br />

GeschwindigkeitModifikator = 0,<br />

DrehgeschwindigkeitModifikator = -1,<br />

LastModifikator = -1,<br />

ReichweiteModifikator = -1,<br />

SichtweiteModifikator = 0,<br />

EnergieModifikator = 1,<br />

AngriffModifikator = 2<br />

Die Sichtweite kann man nicht auch noch erhöhen, - dabei ist gerade sie wichtig, um<br />

möglichst schnell viele Käfer in den globalen Speicher schreiben zu können.<br />

Deshalb werden wir eine zweite Art erzeugen, da reichen sicher 6 – 10 Exemplare,<br />

die Spione. Sie haben einzig und allein die Aufgabe, Käfer zu finden und zu<br />

speichern:<br />

Name = "Spion",<br />

GeschwindigkeitModifikator = 1,<br />

DrehgeschwindigkeitModifikator = 0,<br />

LastModifikator = -1,<br />

ReichweiteModifikator = 0,<br />

SichtweiteModifikator = 2,<br />

EnergieModifikator = -1,<br />

AngriffModifikator = -1<br />

Damit ist es nicht getan! Schließlich sollte eine Ameise die Spielrunden zählen. Dabei<br />

sollte sie aber möglichst nicht von Käfern gefressen werden. Eine Ameise dieser Art<br />

reicht. Wir nennen sie Chronos:<br />

Name = "Chronos",<br />

GeschwindigkeitModifikator = 2,<br />

DrehgeschwindigkeitModifikator = 0,<br />

LastModifikator = 0,<br />

ReichweiteModifikator = 0,<br />

SichtweiteModifikator = -1,<br />

EnergieModifikator = 0,<br />

AngriffModifikator = -1<br />

Die angegebenen Werte sollten Sie nicht als Dogma sehen. Es kann lohnend sein,<br />

damit zu experimentieren.<br />

So erzeugen wir jetzt die <strong>Ameisen</strong>:<br />

public static string BestimmeTyp(Dictionary anzahl)<br />

{<br />

if (anzahl["Chronos"] < 1) { return "Chronos"; }<br />

if (anzahl["Spion"] < 6) { return "Spion"; }<br />

else { return "Kämpfer"; }<br />

}<br />

Auch hier sollten Sie mit der Anzahl der Spione experimentieren.<br />

Die Methoden Sieht(Käfer käfer) und WirdAngegriffen(Käfer käfer) können gleich<br />

gestaltet werden. Ein Vorschlag:<br />

44


switch (Typ)<br />

{<br />

case "Spion":<br />

{<br />

Ortsspeicher.OrtSpeichern(käfer);<br />

GeheWeg<strong>Von</strong>(käfer);<br />

}<br />

break;<br />

case "Kämpfer":<br />

{<br />

Ortsspeicher.OrtSpeichern(käfer);<br />

GreifeAn(käfer);<br />

}<br />

break;<br />

}<br />

case "Chronos":<br />

{<br />

GeheWeg<strong>Von</strong>(käfer); //Falls ein böser Käfer in den Bau kommt!<br />

}<br />

break;<br />

Sie erinneren sich: In jeder Spielrunde wird das Tick-Ereignis aufgerufen. Hier<br />

erreichen wir die <strong>Ameisen</strong> immer und können ihnen daher sehr genau mitteilen, wie<br />

sie sich zu verhalten haben. Wie oben unterscheiden wir die Typen durch einen<br />

switch-case-Block.<br />

Beginnen wir mit den Spionen:<br />

case "Spion":<br />

{<br />

if (Reichweite - ZurückgelegteStrecke - 150 < EntfernungZuBau)<br />

GeheZuBau();<br />

if (AktuelleEnergie < MaximaleEnergie * 2 / 3)<br />

GeheZuBau();<br />

}<br />

break;<br />

Auf diese Weise sorgen wir dafür, dass uns diese wertvolle Spezies nicht dezimiert<br />

wird.<br />

Zu Chronos:<br />

case "Chronos":<br />

{<br />

Ortsspeicher.Schleifenzaehler++;<br />

if (Ortsspeicher.Schleifenzaehler % 500 == 0)//Gedächtnis hält 500 Runden!<br />

{ Ortsspeicher.KäferOrt.Clear(); Ortsspeicher.GeloeschterOrt.Clear(); }<br />

if (EntfernungZuBau > 32) { GeheZuBau(); }<br />

else { BleibStehen(); }<br />

BleibStehen();<br />

}<br />

break;<br />

Der <strong>Ameisen</strong>bau hat einen Radius von 32 Einheiten. Daher kann Chronos innerhalb<br />

dieses Radius ruhig stehen bleiben.<br />

45


Schließlich und endlich zu den Kämpfern. Wir wollen verhindern, dass eine Ameise,<br />

die auf der anderen Seite des Spielfeldes steht, einen Angriff auf den viel zu weit<br />

entfernten Käfer startet. Mehr noch: Die Kämpfer-<strong>Ameisen</strong> sollen alle vorhandenen<br />

Käfer in der Liste prüfen, und zu dem laufen, der am nächsten ist:<br />

case "Kämpfer":<br />

{<br />

if (Ziel == null && Ortsspeicher.KäferOrt.Count > 1)<br />

//Ameise hat kein Ziel und die "Gedächtnisdatenbank" ist nicht leer<br />

{<br />

int entfernung = 2000; int entfernungVariabel = 2000;<br />

int indexMin = 0;<br />

for (int i = 0; i < 60; i++)<br />

{<br />

Käfer käfer0 = Ortsspeicher.LiesKäfer(i);<br />

Aufgabe 7<br />

}<br />

}<br />

if (käfer0 == null) //Käfer0 existiert nicht oder ist zwischenzeitlich gestorben!<br />

Ortsspeicher.OrtLoeschen(käfer0);<br />

else<br />

{<br />

entfernungVariabel = Koordinate.BestimmeEntfernung(this, käfer0);<br />

if (entfernung > entfernungVariabel)<br />

{<br />

entfernung = entfernungVariabel;<br />

indexMin = i;<br />

}<br />

}<br />

käfer0 = null;<br />

Käfer käfer1 = Ortsspeicher.LiesKäfer(indexMin);<br />

if ((!(käfer1 == null)) && (!Ortsspeicher.GeloeschterOrt.Contains(käfer1)) &&<br />

(Koordinate.BestimmeEntfernung(this, käfer1) < 700))<br />

{ GeheZuZiel(käfer1); }<br />

//Letzte Bedingung: Ameise sollte nicht zu weit weg vom Käfer sein.<br />

GeheGeradeaus(50);//sonst bleiben sie nach der Jagd stehen!<br />

if (Reichweite - ZurückgelegteStrecke - 100 < EntfernungZuBau)<br />

GeheZuBau();<br />

if (AktuelleEnergie < MaximaleEnergie * 2 / 3)<br />

GeheZuBau();<br />

}<br />

break;<br />

• Versuchen Sie, die Tick()-Anweisungen für die drei <strong>Ameisen</strong>-Typen<br />

nachzuvollziehen. Wo könnten andere Werte getestet werden. Sehen Sie<br />

weitere Möglichkeiten der Optimierung?<br />

• Vervollständigen Sie mit den obigen Vorgaben die Vorlage.cs , die Sie in<br />

Kämpfer.cs umtaufen.<br />

46


• Testen Sie die Kämpferameisen!<br />

Da können einem die Käfer<br />

schon fast leid tun!!!<br />

Bis zu 45-46 erledigte Käfer<br />

sind so zu erreichen, das<br />

heißt, man kommt auf fast<br />

7000 Punkte! Und das ohne<br />

Nahrung zu sammeln….<br />

(Interessantes Detail am<br />

Rande: Wenn man die<br />

Simulation in Zeitraffer<br />

ablaufen lässt, dann erkennt<br />

man, dass immer dann,<br />

wenn die <strong>Ameisen</strong> ein<br />

Käfer-Ziel ansteuern, die<br />

Geschwindigkeit deutlich<br />

höher ist, als wenn sie<br />

gerade ziellos umherlaufen.<br />

Haben Sie eine Idee, woran<br />

das liegen könnte?)<br />

Kämpfer und Arbeiter mit globalem Gedächtnis<br />

Ganz ohne Nahrungssammler kann ein <strong>Ameisen</strong>volk nicht überleben. Man kann sich<br />

vorstellen, dass das Volk nur überlebt, wenn mindestens 10 000 Punkte erreicht<br />

werden. Also müssen wir mehr als diese 10 000 Punkte erreichen.<br />

Ein erster Schritt wird sein, Chronos das Löschen des Gedächtnisses zu verbieten.<br />

Dies war ja eine freiwillige Einschränkung.<br />

Dann erzeugen wir eine neue Sorte für das Sammeln von Nahrung: Die Arbeiter!<br />

[Typ(<br />

Name = "Kämpfer",<br />

GeschwindigkeitModifikator = 0,<br />

DrehgeschwindigkeitModifikator = -1,<br />

LastModifikator = -1,<br />

ReichweiteModifikator = -1,<br />

SichtweiteModifikator = -1,<br />

EnergieModifikator = 2,<br />

AngriffModifikator = 2<br />

)]<br />

47


[Typ(<br />

Name = "Spion",<br />

GeschwindigkeitModifikator = 1,<br />

DrehgeschwindigkeitModifikator = 0,<br />

LastModifikator = -1,<br />

ReichweiteModifikator = 0,<br />

SichtweiteModifikator = 1,<br />

EnergieModifikator = 0,<br />

AngriffModifikator = -1<br />

)]<br />

[Typ(<br />

Name = "Chronos",<br />

GeschwindigkeitModifikator = 2,<br />

DrehgeschwindigkeitModifikator = 0,<br />

LastModifikator = 0,<br />

ReichweiteModifikator = 0,<br />

SichtweiteModifikator = -1,<br />

EnergieModifikator = 0,<br />

AngriffModifikator = -1<br />

)]<br />

[Typ(<br />

Name = "Arbeiter",<br />

GeschwindigkeitModifikator = 2,<br />

DrehgeschwindigkeitModifikator = -1,<br />

LastModifikator = 2,<br />

ReichweiteModifikator = -1,<br />

SichtweiteModifikator = 0,<br />

EnergieModifikator = -1,<br />

AngriffModifikator = -1<br />

)]<br />

Die Arbeiter müssen schnell sein und viel tragen können. Wieviele Arbeiter und<br />

wieviele Kämpfer man erzeugen soll, muss man ausprobieren. Hier ein Vorschlag:<br />

public static string BestimmeTyp(Dictionary anzahl)<br />

{<br />

if (anzahl["Chronos"] < 1) { return "Chronos"; }<br />

if (anzahl["Spion"] < 5) { return "Spion"; }<br />

if (anzahl["Arbeiter"] < 60) { return "Arbeiter"; }<br />

if (anzahl["Kämpfer"] < 34) { return "Kämpfer"; }<br />

else { return "Arbeiter";<br />

}<br />

Damit der Code gut lesbar wird, erzeugen wir für jeden <strong>Ameisen</strong>typ eine eigene<br />

Klasse. Das spezifische Wartet()- und Tick()-Ereignis kann man dann dort<br />

unterbringen.<br />

Beginnen wir mit der Chronos Klasse:<br />

48


class Chronos<br />

{<br />

public static void Wartet(Ameise ameise)<br />

{<br />

if (Ortsspeicher.daheim == null)<br />

{<br />

ameise.GeheZuBau();<br />

Ortsspeicher.daheim = ameise.Ziel;<br />

ameise.BleibStehen();<br />

}<br />

}<br />

}<br />

Der Wartet()-Abschnitt muss erklärt werden:<br />

Wir wollen die Arbeiter später immer den Zuckerhaufen oder das Obst auswählen<br />

lassen, dass die geringste Entfernung zum Bau hat. So ohne Weiteres verrät das<br />

Interface IKoordinate diesen Ort aber nicht. Daher fügen wir in der Klasse<br />

Ortsspeicher eine IKoordinate daheim an.<br />

public static IKoordinate daheim;<br />

Aus den weiter oben schon erläuterten Gründen muss daheim public und static sein.<br />

Wenn Chronos wartet und daheim noch unbekannt ist, dann lässt man sie mit<br />

ameise.GeheZuBau() zum Bau laufen. Auch wenn sie schon dort ist, kann man jetzt<br />

das Ziel (den Bau) abgreifen und eine Kopie davon, die sich mit daheim aufrufen<br />

lässt, für die weitere Verwendung in den Heap legen.<br />

In der allgemeinen Klasse Allgemein_GedächtnisWahl muss der Wertet()-Abschnitt<br />

dann nur auf die zugehörige Klasse verweisen:<br />

public override void Wartet()<br />

{<br />

switch (Typ)<br />

{<br />

case "Spion":<br />

{<br />

Spion.Wartet(this);<br />

}<br />

break;<br />

case "Kämpfer":<br />

{<br />

Kämpfer.Wartet(this);<br />

}<br />

break;<br />

case "Chronos":<br />

{<br />

Chronos.Wartet(this);<br />

49


}<br />

}<br />

break;<br />

case "Arbeiter":<br />

{<br />

Arbeiter.Wartet(this);<br />

}<br />

break;<br />

}<br />

(Auch wenn dort kein Code für das Wartet()-Ereignis ausgeführt wäre, führt der obige<br />

Aufruf zu keiner Fehlermeldung.)<br />

Und so wartet die Spion-Klasse:<br />

public static void Wartet(Ameise ameise)<br />

{<br />

ameise.GeheGeradeaus(100);<br />

ameise.DreheUmWinkel(Zufall.Zahl(-20, 20));<br />

}<br />

Die konkreten Zahlen (100 und 20) sind ziemlich willkürlich und sollten unbedingt<br />

optimiert werden!<br />

Das Warten()-Ereignis der Arbeiter:<br />

public static void Wartet(Ameise ameise)<br />

{<br />

if (ameise.EntfernungZuBau > 400) //außerhalb gibt es keine Nahrung!<br />

{<br />

ameise.GeheZuBau();<br />

}<br />

}<br />

Aufgabe 8<br />

Schreiben Sie in die allgemeine Klasse Allgemein_GedächtnisWahl die Ereignisse<br />

mit Parameter Sieht(Zucker zucker) , Sieht(Obst obst) und Sieht(Käfer käfer).<br />

Gleichgültig welcher <strong>Ameisen</strong>typ etwas sieht, immer sollen die Koordinaten des<br />

gesehenen Objekts in der Liste gespeichert werden.<br />

Bei Sieht(Käfer, käfer) muss man bedenken, dass Chronos- und Spionameisen<br />

möglichst schnell fliehen sollten. Die Kämpferameisen hingegen greifen sofort an.<br />

(Eine mögliche Teil-Lösung am Ende des Kapitels)<br />

In der gleichen Klasse sollten noch zwei Ereignisse, die in der reinen Kämpferkolonie<br />

unnötig waren, festgelegt werden:<br />

public override void ZielErreicht(Zucker zucker)<br />

{<br />

50


}<br />

Nimm(zucker);<br />

GeheZuBau();<br />

Entsprechend muss man dies auch beim Obst so festlegen.<br />

Kommen wir zu den Tick()-Ereignissen der verschiedenen Klassen:<br />

Bei Chronos, Spion und Kämpfer wurde nichts Wesentliches verändert, sieht man<br />

einmal davon ab, dass Chronos jetzt erst nach 2000 Spielzügen den Käfer-Speicher<br />

löscht. Obst- und Zucker-ArrayList löschen wir hier nicht während eines Spiels.<br />

Bei Arbeiter-<strong>Ameisen</strong> allerdings müssen wir doch etwas mehr nachdenken, damit<br />

genügend Obst und Zucker in den Bau getragen wird:<br />

• Diejenigen Zuckerhaufen und Obststücke, die näher am Bau liegen als<br />

andere, sollen zuerst abgetragen werden.<br />

• Mehr als fünf <strong>Ameisen</strong> sollten nicht als Träger für ein Obststück abgeordnet<br />

werden. Die Arbeit wird durch mehr als fünf Träger nämlich nicht schneller<br />

erledigt.<br />

Hier eine Möglichkeit dies zu realisieren:<br />

public static void Tick(Ameise ameise)<br />

{<br />

if (ameise.Reichweite - ameise.ZurückgelegteStrecke - 50 < ameise.EntfernungZuBau)<br />

ameise.GeheZuBau();<br />

if (ameise.AktuelleEnergie < ameise.MaximaleEnergie * 2 / 3)<br />

ameise.GeheZuBau();<br />

if (ameise.Ziel == null)<br />

{<br />

int entfernungZumBau = 2000; int entfernungZumBauVariabel = 2000;<br />

int indexMin = 0;<br />

if (Ortsspeicher.ZuckerOrt.Count > -1)<br />

{<br />

for (int i = 0; i < 20; i++)<br />

{<br />

Zucker zucker0 = Ortsspeicher.LiesZucker(i);<br />

if ((zucker0 == null) || (zucker0.Menge < 15))<br />

{<br />

Ortsspeicher.OrtLoeschen(zucker0);<br />

}<br />

else<br />

{<br />

entfernungZumBauVariabel = Koordinate.BestimmeEntfernung(Ortsspeicher.daheim, zucker0);<br />

if (entfernungZumBau > entfernungZumBauVariabel)<br />

{<br />

entfernungZumBau = entfernungZumBauVariabel;<br />

51


}<br />

}<br />

}<br />

}<br />

}<br />

indexMin = i;<br />

zucker0 = null;<br />

Zucker zucker1 = Ortsspeicher.LiesZucker(indexMin);<br />

if ((ameise.AktuelleLast == 0) && (zucker1 != null))<br />

{<br />

ameise.GeheZuZiel(zucker1);<br />

}<br />

if (Ortsspeicher.ObstOrt.Count > -1)<br />

{<br />

for (int i = 0; i < 20; i++)<br />

{<br />

Obst obst0 = Ortsspeicher.LiesObst(i);<br />

if ((obst0 == null) || (!ameise.BrauchtNochTräger(obst0)))<br />

{<br />

Ortsspeicher.OrtLoeschen(obst0);<br />

}<br />

else<br />

{<br />

entfernungZumBauVariabel = Koordinate.BestimmeEntfernung(Ortsspeicher.daheim, obst0);<br />

if (entfernungZumBau > entfernungZumBauVariabel)<br />

{<br />

entfernungZumBau = entfernungZumBauVariabel;<br />

indexMin = i;<br />

}<br />

}<br />

obst0 = null;<br />

}<br />

Obst obst1 = Ortsspeicher.LiesObst(indexMin);<br />

if ((ameise.AktuelleLast == 0) && (obst1 != null) )<br />

{ ameise.GeheZuZiel(obst1);<br />

Ortsspeicher.n[indexMin] = Ortsspeicher.n[indexMin] + 1;<br />

}<br />

if (Ortsspeicher.n[indexMin] > 4)<br />

{<br />

Ortsspeicher.OrtLoeschen(obst1);<br />

Ortsspeicher.n[indexMin] = Ortsspeicher.n[indexMin+1] ;<br />

Ortsspeicher.n[indexMin + 1] = Ortsspeicher.n[indexMin + 2];<br />

Ortsspeicher.n[indexMin + 2] = Ortsspeicher.n[indexMin + 3];<br />

Ortsspeicher.n[indexMin + 3] = Ortsspeicher.n[indexMin + 4];<br />

Ortsspeicher.n[indexMin + 4] = Ortsspeicher.n[indexMin + 5];<br />

}<br />

}<br />

}<br />

52


Aufgabe 9<br />

• Welche Aufgabe hat daheim, wozu dient int[ ] n ?<br />

public static IKoordinate daheim;<br />

public static int[ ] n = new int[ ] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,.….<br />

• In welchen Fällen werden die ListArrays von Zuckerort, Obstort und Käferort<br />

gelöscht?<br />

• Versuchen Sie den obigen Programmcode nachzuvollziehen.<br />

• Experimentieren Sie mit den Vorgaben. Vielleicht erreichen Sie ja dann mehr<br />

als 15960 Punkte!<br />

Was man noch tun könnte…<br />

Hier endet Kapitel 5.0<strong>2.</strong> Das heißt aber nicht, dass es nichts mehr zu tun gäbe! Wenn<br />

Sie weitermachen wollen, hier einige Vorschläge:<br />

• Die eigenen <strong>Ameisen</strong> auf AntMe! Version 1.6 umschreiben. Dort gibt es außer<br />

Käfern noch Wanzen, die den armen <strong>Ameisen</strong> zusetzten. Auch die Punktevergabe<br />

setzt etwas andere Akzente.<br />

• Ein eigenes Plugin schreiben.<br />

• Die Netzversion verwenden, um Ihre <strong>Ameisen</strong> auch gegen Konkurenten fit zu<br />

machen.<br />

Ideen und Anregungen bekommt man auf der Seite von AntMe! : http://antme.net/<br />

53


Lösungen<br />

Lösung von Aufgabe 2:<br />

public override void Sieht(Obst obst)<br />

{<br />

if (AktuelleLast == 0 && BrauchtNochTräger(obst))<br />

{<br />

GeheZuZiel(obst);<br />

}<br />

}<br />

public override void ZielErreicht(Obst obst)<br />

{<br />

Nimm(obst);<br />

GeheZuBau();<br />

}<br />

Lösung von Aufgabe 3:<br />

public override void Sieht(Käfer käfer)<br />

{<br />

if (AktuelleLast > 0) LasseNahrungFallen();<br />

if (AnzahlInSichtweite > 5) GreifeAn(käfer);<br />

else GeheWeg<strong>Von</strong>(käfer, 80);<br />

}<br />

Lösung von Aufgabe 6:<br />

private void loeschen_button_Click(object sender, EventArgs e)<br />

{<br />

string Zeichenfolge = textBox<strong>2.</strong>Text;<br />

ListeArrayList.StringLoeschen(Zeichenfolge);<br />

}<br />

private void ausgabe_button_Click(object sender, EventArgs e)<br />

{<br />

int n = Convert.ToInt32(textBox3.Text);<br />

string Zeichenfolge = ListeArrayList.GibString(n);<br />

textBox4.Text = Zeichenfolge;<br />

}<br />

private void button4_Click(object sender, EventArgs e)<br />

{<br />

ListeArrayList.StringListe.Clear();<br />

}<br />

54


private void sortieren_button_Click(object sender, EventArgs e)<br />

{<br />

ListeArrayList.StringListe.Sort();<br />

}<br />

private void anzahl_button_Click(object sender, EventArgs e)<br />

{<br />

int n = ListeArrayList.StringListe.Count;<br />

textBox3.Text = Convert.ToString(n);<br />

}<br />

Teillösung von Aufgabe 8:<br />

public override void Sieht(Zucker zucker)<br />

{<br />

switch (Typ)<br />

{<br />

case "Spion":<br />

{<br />

Ortsspeicher.OrtSpeichern(zucker);<br />

}<br />

break;<br />

}<br />

}<br />

case "Kämpfer":<br />

{<br />

Ortsspeicher.OrtSpeichern(zucker);<br />

}<br />

break;<br />

case "Arbeiter":<br />

{<br />

Ortsspeicher.OrtSpeichern(zucker);<br />

}<br />

break;<br />

55

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!