HARALD MELCHER

Flattiverse - Raumschiffe Programmieren in C#

Flimmerfrei anzeigen

Wenn das Gui Sonnen und andere Körper zeichnet, löscht es zuerst den Schirm und zeichnet dann alle Körper neu. Das flimmert, weil sich das Bild immer von leer zu gefüllt neu aufbaut.

Eine Abhilfe ist das Doppel-Puffern: Man zeichnet in einen Buffer im Hintergrund, der nicht angezeigt wird. Wenn das Bild im Hintergrund fertig ist, kopiert man es mit einer einzigen Operation auf den Vordergrund und ersetzt so das bisherige Bild. Da sich die meisten Körper nicht bewegt haben, flimmern sie dadurch nicht. Körper, die sich bewegt haben, erscheinen an der neuen Position.

Gui.cs

Der Konstruktor von Gui erstellt eine Bitmap bitmap in der benötigten Größe (Width und Height) und fügt folgenden Handler an das Load-Event und an das Resize-Event von Gui an:

private void Gui_Load(object sender, EventArgs e) {
    bitmap?.Dispose();
    bitmap = new Bitmap(Width, Height);
}

Dann setzt er im Konstruktor folgende Parameter:

SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);

Die OnPaint-Methode verwendet dann statt des Graphics-Kontextex aus e.Graphics den Graphics-Kontext der Bitmap mittels

Graphics g = Graphics.FromImage(bitmap)

Alle anderen Zeichenbefehle bleiben gleich.

Nach dem letzten Zeichenbefehl erfolgt noch das “Blitten”, dh. Kopieren der Bitmap auf den eigentlichen Schirm, dessen Graphics-Kontext noch in e liegt:

e.Graphics.DrawImage(bitmap, 0, 0);
Flattiverse - Raumschiffe Programmieren in C#

Anzeigen

Gui.cs

In der Gui-Klasse nimmt die Methode DisplayUnits(List<Unit> units) die Liste entgegen und erstellt eine lokale Kopie von der Liste. Danach ruft sie Invalidate() auf, damit Windows bei nächster Gelegenheit die Gui-Form neu zeichnet. Wie Zeichnen geht, erfährt die Form, indem wir ihre Methode OnPaint überschreiben.

OnPaint(PaintEventArgs e) erhält schon eine Graphics-Komponente, mit der man zeichnen kann: Graphics g = e.Graphics;.

Nun kann man durch die Liste der zu zeichnenden Units gehen und für jede Unit mit g.DrawEllipse() einen Kreis zeichnen.

Achtung: g.DrawEllipse() zeichnet den Kreis vom linken oberen Rand des einschließenden Quadrats, nicht vom Mittelpunkt!

Flattiverse - Raumschiffe Programmieren in C#

Umschauen

Body.cs

Scannen erfolgt von einem Schiff aus. Der Aufruf von NewShip erzeugt ein Schiff im angegebenen Universum.

await server.Player.Universe.NewShip("SuperShip")

Wenn das Schiff im Universum ist, kann es vom Schiff aus sichtbare Körper im Weltall sehen. Dazu hängt man einen Handler an server.ScanEvent, den der Connector immer aufruft, wenn neue Körper sichtbar sind.

Ab jetzt erfolgen immer dann Aufrufe des Handlers (s.o.), wenn das Schiff neue Körper sieht.

Die Handler-Methode erhält einen Flattiverse-ScanEvent, der über gescannte Körper informiert:

private void scanEvent(FlattiverseEvent @event) { ... }

Zur Anzeige neu gescannter Köper dient die von FlattiverseEvent abgeleitete Klasse NewUnitEvent. Eine einfache if-Anweisung filtert (zunächst) nur diese aus und ein Cast auf NewUnitEvent stellt sicher, dass wir auf dessen Eigenschaft Unit zugreifen können. Das ist dann ein Körper, der in der Nähe des Schiffes fliegt. Unit hat dabei z.B. den Typ Sun, Planet oder Target.

Die Body-Klasse kann diese Körper in einer Liste units sammeln und, wenn sie sich geändert hat, der Gui-Klasse (deren Referenz sie durch ConnectGui hat) durch Aufruf der Methode gui.DisplayUnits(units) zum Anzeigen geben.

Eine Herausforderung ist, dass die Körper direkt um’s Schiff herum schon NewUnitEvents triggern, während body.Start() noch läuft und das Gui möglicherweise noch nicht existiert. Das heißt, bei der Zuweisung des Gui zum Body ist die Liste schon gefüllt und es gibt momentan keine neuen Körper, die Events triggern.

Deshalb ruft ConnectGui die Methode gui.DisplayUnits(units) auf, damit die Anzeige auch die vor Erstellen des Guis bereits bekannten Körper kennt.

Flattiverse - Raumschiffe Programmieren in C#

Verbindung zum Server

Änderungen im Programm-Gerüst

Program.cs

Die von Visual Studio erstellte Programm-Klasse erzeugt im Aufruf von Application.Run(new Form1()) die Form mit der Benutzeroberfläche. Hier kann man Oberfläche und “Mechanik” des Raumschiffs trennen.

Zunächst erhält die Form-Klasse einen anderen Namen als Form1 (bei mir Gui), damit sie ihrer Bedeutung als Graphical User Interface gerechter wird. Beim Umbenennen zickt Visual Studio möglicherweise, so dass das eine Aufgabe für den Datei-Explorer wird.

Die Instanziierung der Gui-Klasse erfolgt in einem getrennten Schritt vor dem ganzen Application-Code und Application.Run(gui), so dass dazwischen noch Erstellen und Starten des Raumschiffes passt.

Der Raumschiff-Rumpf (bei mir Body) erfährt nach Erstellung im Application-Teil das Gui durch ConnectGui(gui), damit er darauf zugreifen kann.

Beim Starten des Rumpfs verwenden wir das await-Schhlüsselwort, weil das die erste eigene asynchrone Methode wird. Das hat zur Folge, dass die Main-Methode nun auch den Wert async Task haben muss.

Der Raumschiff-Körper

Body.cs

Die Start-Methode ist vom Typ async Task und erstellt zuerst eine neue Instanz des Server-Objekts aus der Connector-DLL. Über sie läuft die Kommunikation mit dem Server.

await server.Login(username, password) loggt die Spielerin oder den Spieler ein.

Welche Universen der Server kennt, liefert seine Universes-Property. Sie kennt auch die Galaxien zu jedem Universum.

In ein Universum kommt man mit Join, das eine Teamfarbe des jeweiligen Universums als Parameter hat:

Universe universe 
    = server.Universes["Beginners Course"]; 

await universe
    .Join(universe.Teams["None"]);

Damit kann unser Programm jetzt

  • sich mit dem Flattiverse-Server verbinden
  • sich mit Userid und Passwort anmelden
  • sich die Liste der Universen holen
  • in jedem Universum die Galaxien sehen
  • die passenden Teamfarben pro Universum holen

Flattiverse - Raumschiffe Programmieren in C#

Vorbereiten der Entwicklungs-Umgebung

Der Beispielcode hier ist mit Microsoft Visual Studio erstellt, das in verschiedenen Lizenzmodellen – auch frei – erhältlich ist.

Im Visual Studio das Projekt als Windows Forms App (.NET Core) anlegen. Der .NET Core Code läuft im allgemeinen schneller und der Code der Bibliotheken ist frei und Open Source. Im Moment hat Visual Studio allerdings für .NET Core noch keinen Form-Designer, so dass man entweder Forms “von Hand” erstellt oder eben doch ein Windows Forms App (.NET Framework) anlegt.

Neues Projekt erstellen

Mein Schiff habe ich hier Schulschiff genannt, weil es das Schiff zum Mitlernen ist. Gib deinem Schiff einen tolleren Namen!

Neues Projekt konfigurieren

Im weiteren Verwende ich .NET Core 3.1 – das ist je nach Installationsart noch ein weiterer Download und Installationsschritt. Damit hat sich die C#-Sprachversion auf 8.0 eingestellt. Da ich Konsolenausgabe sehen will, erstelle ich den Ausgabentyp Konsolenanwendung. Eine Forms-Oberfläche geht später dennoch. Die Eigenschaften unter Projekt → Eigenschaften sehen dann so aus:

Der Connector für die Verbindung zum Server ist im Moment über die Flattiverse Dokumentationsseite erreichbar – er kommt in das Connector-Verzeichnis im Projekt:

Connector im Verzeichnis

Ein Rechtsklick auf Abhängigkeiten öffnet den Verweis-Manager, in dem wir die Connector.dll im Verzeichnis suchen und einbinden.

Abhaengigkeiten
Verweis-Manager

Einmal auf RUN gedrückt zeigt uns, ob wir alles einigermaßen richtig zusammengestellt haben.

Startrek in C

Verbesserter Parser

Im bisherigen Code tauchen Abschnitte mehrfach auf – das sind Verbesserungsmöglichkeiten. Gerade die Anweisungen zum

  • Ausgeben eines Fragetextes (ohne Linefeed)
  • Flushen des Ausgabebuffers (der sonst nur bei Linefeed geflusht wird)
  • Einlesen der Antwort der Spielerin
  • Entfernen des schließenden Linefeeds

tauchen mehrfach auf. Lagere sie in eine Funktion void ask(char* question, char* buffer, int bufsize) aus!

Die Funktion parse_command(char* cmd) überprüft bislang alle Möglichkeiten in getrennten if-Anweisungen. Dies ersetzt du durch eine Tabelle mit Kommandonamen und den passenden enums. Damit kannst du auch einfach mehrere unterschiedliche Bezeichnungen für das gleiche Kommando einführen.

Außerdem wird der Code dann das Kommando nur auf soviele Buchstaben vergleichen, wie die Benutzereingabe lang ist.

Zunächst wird eine kleine Hilffunktion int command_length(char *cmd) die Länge des Kommandos ermitteln, indem sie die Zeichen bis zum ersten Leerzeichen, Zeilenende oder Stringende zählt.

Ein typedef für den Typ command_list_entry beschreibt einen einzelnen Eintrag in der Kommandoliste, die ein Array command_list dieser Einträge ist.

typedef struct {
    char* command;
    int command_code;
} command_list_entry ;

In den Initialisierer des (globalen) command_list-Array kommen jetzt Paare aus Kommandos und den dazu passenden enums.

command_list_entry command_list[] = {
        {"exit", exit},
        {"help", help},
        {"srs", short_range_scan},
        {"lrs", long_range_scan}
};

Die Funktion parse_command(char* cmd) muss jetzt nur noch alle Einträge im command_list-Array durchgehen (s.u.) und einen Stringvergleich (mit Längenangabe aus dem Kommando) durchführen. Passt der Vergleich, liefert sie die dazugehörige enum zurück. Passt keiner der Strings, liefert sie das enum für unbekannt zurück.

Die Zahl der Einträge in einem Array errechnet sich aus der Größe des Arrays geteilt durch die Größe eines Array-Eintrags (also beispielsweise des ersten).

int num_entries = sizeof(command_list) / sizeof(command_list[0]);

Jetzt kannst du neue Einträge in die Kommandoliste einbauen, zum Beispiel “quit” als Alternative zu Exit.

Startrek in C

Long Range Scan

Unser Spiel lernt das Kommando lrs (“Long Range Scan” ). Dazu

  • erweitern wir parse_command() um lrs
  • erweitern wir unsere Hauptschleifen-switch um einen eventuellen Aufruf von do_lrs()

In der galaxy.h

  • definieren wir einen neuen Typ lrs_entry als struct mit drei Werten: der Zahl der Klingonen, der Zahl der Sonnen und der Zahl der Starbasen
  • definieren wir einen neuen Typ lrs_result als 3×3-Array von lrs_entrys

galaxy.c erhält die neue Funktion void galaxy_lr_scan(lrs_result scan), die in ein mitgeliefertes Ergebnisarray die Scans des aktuellen Quadranten und seiner 8 Nachbarn einträgt.
Hier hilft die bereits geschriebene Funktion quadrant_id(), die Ids der jeweiligen Quadranten zu finden. (Tipp: die um eine Quadrantenbreite verschobenen Positionswerte des Schiffs liegen im Nachbarquadranten).
Eine doppelte Schleife füllt dann schnell das 3×3-Array mit den Werten der zu schreibenden lrs_entry lr_scan_one_quadrant(int quadrant_to_scan).

rs_entry lr_scan_one_quadrant(int quadrant_to_scan) bereitet einen leeren lrs_entry vor und initialisiert ihn!

  • geht durch alle Units und schaut, ob sie im fraglichen Quadranten liegen
  • falls das zutrifft, schaut sie nach dem Typ der Unit und zählt entweder Klingonen, Sterne oder Sternbasen mit
  • liefert den gefüllten lrs_entry zurück (Wertetyp)

Die Funktion do_lrs() in main.c soll das Long-Range-Scan-Ergebnis anfordern und anzeigen. Dazu

  • Richtet sie eine Variable vom Ergebnistyp ein – glücklicherweise sieht der einfacher als, weil es dazu ein typedef gibt
  • übergibt diese Variable der Funktion galaxy_lr_scan()
  • findet nach deren Beendigung den Ergebnistyp gefüllt mit den Zahlen der Units in den umliegenden Quadranten
  • zeigt diese in zwei verschachtelten Schleifen an

Hierbei könnte (muss aber nicht) eine Funktion char* lr_symbol_of(lrs_entry entry, char *buffer) den jeweiligen Eintrag formatiert in einen übergebenen Ergebnisbuffer schreiben. Dies verkleinert den Code und verbessert seine Lesbarkeit. Die Funktion liefert einen Zeiger auf den Buffer zurück.
(Warum liefert sie den Zeiger auf den Buffer zurück, die aufrufende Funktion kennt doch ihren Buffer?)

Und so könnte der Long-Range-Scan dann aussehen:

Startrek in C

Short Range Scan

Es wird ein Kommando mehr geben: “srs” oder “shortrangescan”. Es soll alle Objekte im All “grafisch” anzeigen, die im gleichen Quadranten wie das Raumschiff liegen.

  • dazu ergänzt du zunächst das enum mit den Kommandos um short_range_scan.
  • die Funktion parse_command(...) soll das neue Kommando erkennen und das richtige enum zurückgeben.
  • der switch in der Hauptschleife soll auf das neue Kommando mit Aufruf von do_srs() reagieren. Diese Funktion führt den eigentlichen Scan-Code aus.

Der Short Range Scan liefert ein Array zurück, das für alle Sectoren im Quadranten des Raumschiffs angibt, was darin enthalten ist. Das sind also 8×8 Elemente – ein zweidimensionales Array. Zur besseren Lesbarkeit dient hier ein enum:

typedef char srs_result[8][8];

Der Code soll nun die zu schreibende Funktion void galaxy_sr_scan(srs_result) aufrufen. Da nach Beendigung dieser Funktion der Verbleib der Werte in der Funktion unbestimmt ist, erzeugt das Programm dieses 8×8-Array in der aufrufenden Funktion do_srs() und übergibt es als Parameter beim Aufruf von galaxy_sr_scan().


void galaxy_sr_scan(srs_result) setzt zunächst alle Positionen im Ergebnisarray auf leer (' ').

Dann berechnet sie mit int quadrant_id(short x, short y) eine eindeutige ID für den Quadranten der Position des Schiffs. Hier empfiehlt sich z.B. Spalte * 8 + Zeile des Quadranten. Sicherheitshalber regelt die quadrant_id()-Funktion auch gleich, was sie mit Parametern macht, die außerhalb des Spielfelds liegen. (Da das Verfahren für x und y das gleiche ist, könnte hier sogar eine Funktion short legalize(short coord) Code vereinfachen helfen.)

Eine einfache Schleife kann nun durch alle Units gehen und für jede:

  • den Quadranten der Unit berechnen
  • vergleichen, ob er mit dem Schiffsquadranten übereinstimmt (dann sind Unit und Schiff im gleichen Quadranten)
  • gegebenenfalls im Ergebnisarray den Typ auf den der aktuellen Unit setzen (Achtung: Koordinatenumrechnung auf “innerhalb des Quadranten”)

Zurück in der do_srs()-Funktion braucht diese Funktion nur noch die Scanwerte anzeigen:

  • eine Zeile mit Koordinatenwerten und etwas Abstand dazwischen
  • dann für jede der 8 Scan-Zeilen eine Zeile mit den 8 Scanwerten aus dieser Zeile. Da die Scanwerte aus einzelnen Buchstaben bestehen, könnte eine Funktion char* symbol_of(char c) die Werte mit einem Case-Statement in schöne Strings (s.u.) umrechnen.
  • und am Ende noch einmal eine Zeile mit Koordinatenwerten zur besseren Orientierung

Als Vorschlag für die Strings:

S<*>Schiff
**Stern
K###Klingone
B>!<Basis
spaceunbelegt


Das Ganze könnte dann so aussehen:

Startrek in C

Schöpfung des Weltalls

Körper im Weltall

Nachdem du bei bool bereits gesehen hast, dass man existierenden C-Type neue Namen geben kann, kombinieren wir das nun mit einem struct.

Ein struct ist ein Behälter, der ein oder mehrere Variable aufnehmen kann und sie mit Namen belegen, die nur in Verbindung mit dem struct gelten.

Der C Compiler behandelt struct wie die eingebauten Werte-Typen (z.B. int oder long). Das heißt, wenn ein struct als Argument einer Funktion auftaucht, übergibt der C-Compiler eine Kopie des struct – und damit kann die aufgerufene Funktion die übergeben Kopie zwar ändern, aber das “Original” bleibt unverändert.

Soll die aufgerufene Funktion auch das Original verändern können, übergibt man statt des structs (=Kopie) einen Zeiger auf das Original. (Genaunommen ist das dann eine Kopie des Zeigers, aber auch die zeigt auf das Original)

Die galaxy.h erhält nun ein struct, das dem typedef als Vorlage für einen neuen Typ-Namen dient:

typedef struct {
    char type;
    short x;
    short y;
} unit;

Auf Variablen in einem struct greift man mit der Punkt-Schreibweise zu, also zum Beispiel

unit klingon;
klingon.type = 'K';
klingon.x = 13;
klingon.y = 27;

Ist die Variable aber ein Pointer auf ein strukt, geht die Pfeilschreibweise, die weiter unten nochmal auftaucht.

unit* actual_unit = &units[4];
actual_unit->x = 16;
actual_unit->y = 3;

Initialisieren des Weltalls II

Aus main.c ruft der Code bereits das galaxy_init() von galaxy.c auf. Dieses Gerüst bekommt nun Leben:

In den Kopf von galaxy.c kommen drei #defines, die die Zahl der Sonnen auf 30, die Zahl der Klingonen auf 17 und die Zahl der Starbases auf 3 festlegen.

Ebenfalls vor die Funktionen kommt das Array units für die Körper im Weltall. Es muss ein Schiff und alle Sonnen, alle Klingonen und alle Starbases aufnehmen können, die alle vom neudefinierten Typ unit sind. Das sind 30+17+3+1 = 51 Units.

Dazu kommt eine Variable units_top_pos, in der das Programm die Zahl der Units mitzählt, die der Code bereits im Array eingefügt hat. Da noch keine Units im Array sind, startet ihr Wert bei 0.


Später werden wir auch so oft auf das Schiff (das dann erstes Element im Array ist) zugreifen, dass wir eine eigene Variable ship, die auch ein Unit ist, einführen. Dadurch vereinfacht sich der Zugriff auf das Schiff, das im ersten Element (index 0) des units-Arrays liegt.

Wenn wir dieser Variablen ship den Wert des ersten Arrayelements zuweisen würden, dann würde dadurch eine Kopie entstehen und wir könnten zwar Schiffspositionen in der Variablen ship (der Kopie) verändern, im Array wäre das erste Element aber unverändert. Daher erhält diese Variable den Typ (unit* ) und zeigt auf das erste Element. Tipp: Die Zuweisung von ship auf die Position des ersten Array-Elements geht sogar schon zu Compilezeit.


void galaxy_init() ruft insgesamt vier mal die Funktion void place_units(int count, char type) auf: Für 1 Ship, 30 Sonnen, 17 Klingonen und 3 Basen. Dabei gibt der erste Parameter die Zahl der zu erzeugenden Units an und der zweite Parameter das Kürzel für ihren Typ:

 S: Ship
*: Sonne
K: Klingone
B: Base

void place_units(int count, char type) wirft zuerst den Zufallszahlengenerator mit srand(time(0)); an. Dann führt sie für jede zu erstellende Unit aus:

  • die Unit in units, die die oberste im Array ist (die neu zu erstellende), erhält den im Parameter gewünschten Typ
  • die Funktion place_unit(&units[units_top_pos]) platziert die Unit auf einen freien Platz im Weltall. Sie erhält eine Referenz auf die zu platzierende Unit übergeben (und keine Kopie), weil sie den Wert der betreffenden Unit im Array ändern soll.
  • die Position des obersten belegten Elements im Array hat sich dadurch verändert…

void place_unit(unit* u) versucht, eine Unit im All unterzubringen. Sie erzeugt dazu in zwei Variablen für die x- und y-Koordinate mit rand() Zufallszahlen, die sie auf den Bereich von 0…63 einschränkt – die Spielfeldgröße. Dies wiederholt sie so lange, bis die Funktion char unit_type_at(x, y) angibt, dass die Position noch frei ist, indem sie ein Leerzeichen zurückgibt. Dann weißt place_unit() den Wert der Variablen x und y den Positionswerten der übergebenen Unit zu. Dazu gibt es zwei Möglichkeiten

  1. (*u).x = x; dereferenziert den Pointer und verwendet die bekannte Schreibweise für ein struct-Element
  2. u->x = x; ist die Pointer-Schreibweise – sie lässt schon eine Objektorientierung ahnen

char unit_type_at(int x, int y) gibt an, welchen Typ die Unit an der angegebenen Position hat. Dazu geht sie alle bis dahin eingetragenen Units durch und vergleicht die Koordinaten. Sind sie gleich wie die angefragten, dann liegt da schon eine Unit und die Funktion liefert dern Typ zurück. Ist bis zum Ende der Units im Array keines an der Position gewesen, dann ist das Feld noch frei und die Funktion liefert ein Leerzeichen (' ') zurück.


Eine probeweise in galaxy_init() eingebaute Schleife, die alle Objekte mit Typ und Position ausgibt, zeigt, ob das Beleben des Weltalls funktioniert hat.

Startrek in C

Kommandos eingeben

Die Hauptschleife des Programms nimmt jeweils ein Kommando entgegen, und arbeitet es ab. So lange, bis das Kommando das “Ende”-Kommando ist. Das klingt schon ziemlich nach einer do...while-Schleife.

Vor die Schleife kommt eine bool-Variable running, die die Ende-Bedingung der Schleife darstellt. Die Schleife arbeitet so lange, bis sie das Ende-Kommand erhält: Der Kommando-Parser in der Schleife setzt dann running auf FALSE und die Schleife beendet sich. Daher ist running mit TRUE initialisiert.

In die Schleife kommt:

  • eine Eingabeaufforderung der Art “Gib Kommando:”, gefolgt von fflush(), um den Puffer zu leeren. Das ist Copy und Paste von oben.
  • Einlesen eines Eingabestrings mit fgets() in einen Buffer command, und Entfernen des abschließenden Newline. Das ist Copy und paste von oben.
  • der Aufruf der zu schreibenden Funktion lowercase(char*) (s.u.), die den String an der Adresse des übergebenen Stringpointers in Kleinbuchstaben umsetzt.
  • der Aufruf der zu schreibenden Funktion int parse_command(char*) (s.u.), die versucht, das Kommando im Argument zu erkennen, und den Commande-Code (ein enum (s.u.)) für das erkannte Kommando zurückgibt.
  • ein switch-Statement, das je nach Kommando etwas anderes tut. Du wirst mit dem Kommando für exit anfangen: es soll running auf FALSE setzen, damit sich die Schleife beenden kann. Denk dran, die Statements eines case des switch-Befehls mit break abzuschließen.

void lowercase(char *) geht Zeichen für Zeichen bis zum Stringende ('\0') durch und konvertiert jedes Zeichen in Kleinbuchstaben – es überschreibt dabei die vorhanden Großbuchstaben.


Enums sind menschenverständliche Zahlenwerte. Vor Einführung der enums gab es in C nur #define als Möglichkeit, Zahlen einen Namen zuzuweisen. Du verwendest für die Kommandos ein Enum, das den enum-Bezeichnungen Werte ab Null aufsteigend zuweist:

enum {unknown, exit, help};

Damit erhält unnown den Wert 0, exit den Wert 1 und help den Wert 2.


int parse_command(char* command) prüft ab, ob die ersten drei Buchstaben des Kommandos mit einem der vorgegebenen Kommandos übereinstimmt und gibt das passende Enum zurück. (Die Funktion void strncmp(char*, char*, long) aus <string.h> ist dein Freund.)

  • wenn das Kommando mit “exi” anfängt, dann gib das Enum exit zurück
  • wenn das Kommando mit “hel” anfängt, dann gib das Enum help zurück
  • hier baust du weitere Kommandos ein

Jetzt müsste das Kommando “exit” bereits funktionieren. Das Kommando “help” ruft die Funktion void do_help() auf, die erklärt, welche Kommandos es gibt – und vielleicht auch ein bisschen, wie das Spiel funktioniert. Mit jedem neuen Kommando erweiterst du die do_help-Funktion um eine Erklärung desselben.