2. Von objektorientierten Ameisen
2. Von objektorientierten Ameisen
2. Von objektorientierten Ameisen
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