8.9 KiB
Vorgehen beim Programmieren
Montag, 5. Februar 2018
12:55
https://www.mikrocontroller.net/topic/57205
Ein Beispiel:
Dein Freund und du wollen Schach spielen. Dazu schreibst
du ein Pgm, dass euch dabei die Arbeit abnimmt. Es
geht also nicht darum, dass das Pgm selbst Schach spielt
sondern es soll nur die Züge entgegennehmen, eine Anzeige
updaten, prüfen ob die Züge gültig sind, ev ein
Protokoll ausdrucken, die Figuren bewegen. Kurz und
gut: es soll das Schabrett simulieren.
Also was haben wir:
Brett das eigentliche Schachbrett
Liste von Zügen die brauchen wir um das Protokoll
drucken zu können
einen Zug der auszuführende Zug. So ein Zug hat
auch ein Eigenleben. Er besteht aus
den Positionen von wo der Zug weggeht
und wo der Zug hinführt.
Ausserdem speichern wir noch welche Figur
das war. Ergo haben wir noch
Position Wie wird am Brett eine Position gekennzeichnet
Figur Eine Auflistung aller möglichen Figuren
Farbe Die Farbe der Figur
Anzeige Dort wird der aktuelle Stand ausgegeben.
Zugprüfer Das ist weniger offensichtlich aus der
Beschreeibung von oben. Aber wenn wir einen
Zug prüfen wollen, muss es auch eine Maschine
geben, die dies tut.
Spiel Dieses Objekt hält alles zusammen. Es nimmt
Eingaben entgegen (könnte man auch in ein
eigenes Objekt auslagern) und veranlasst, dass
die Objekte zu arbeiten anfangen.
Hab ich noch was vergessen? Möglich. Wir werden im Lauf der Zeit
schon noch drauf kommen, was fehlt.
OK. Welche Eigenschaften haben diese Einzelteile, woraus
bestehen sie, welche Aufgaben haben sie?
Brett: Das Brett ist eine Matrix. In jedem Schnittpunkt (also
in jedem Feld) dieser Matrix kann eine Figur sein oder auch nicht.
Was uns an dieser Stelle zb. überhaupt nicht interessiert ist,
ist ob das Feld Weiss oder Schwarz ist. Für das Spiel an sich
hat das überhaupt keine Bedeutung. Ob weiss oder schwarz ist etwas
worum sich die Anzeige kümmern muss.
Ansonsten muss das Brett nur einen Zug ausführen. Es tut dies
indem die 'Figur' von einem Feld auf ein anderes Feld verschoben
wird.
Liste von Zügen: Dieses Teil hat administrativen Charakter.
Ein Zug wird vom Benutzer über die Eingabe eingegeben und
nach erfolgter Prüfung auf Gültigkeit der Zugliste übergeben.
Die Zugliste speichert den Zug und veranlasst, dass er am Brett
ausgeführt wird. Es mag aber auch spezielle Eingaben geben, die
veranlassen, dass ein Zug zurückgenommen wird bzw. ein zurück-
genommener Zug wieder ausgeführt wird. Dann läuft die Zugliste
zu Höchstform auf: Sie kennt ja die vergangenen Züge und kann
daher diese am Brett rückgängig machen. Natürlich manipuliert
die Zugliste die Figuren am Brett nicht selbst. Wie kommt sie
denn dazu. Aber die Zugliste kann das Brett anweisen einen
Zug durchzuführen.
Ein Zug: Das ist ein eher abstraktes Gebilde. Es ist eigentlich
nur ein Datencontainer der aus 2 Positionen und einer Figur
besteht. Die Bedeutung soll sein: Figur xy zieht von Feld ab
nach Feld cd.
Position: Wieder ein abstraktes Gebilde. Es speichert
Brettkoordinaten und kann Auskunft darüber geben, ob die
gespeicherte Brettkoordinate gültig ist.
Figur: Eine einfache Auflistung, welche Figuren es gibt. Dazu
kommt noch die Farbe der Figur
Anzeige: Aufgabe der Anzeige ist es, das Brett und die Stellung
der darauf befindlichen Figuren anzuzeigen. Dazu muss man der
Anzeige natürlich das Brett zur Verfügung stellen.
Zugprüfer: Aufgabe des Zugprüfers ist es, abzuklären ob ein
bestimmter Zug in der momentanen Brettsituation möglich und
gültig ist. Dazu braucht der Zugprüfer natürlich den zu
untersuchenden Zug und das Brett (das ja die aktuelle Situation
hält). Daraus folgt natürlich auch, dass es eine Möglichkeit
geben muss, das Brett nach dem Inhalt von bestimmten Feldern
zu befragen.
Bleibt nur noch das Spiel. Seine Aufgabe ist es zb, die
Benutzereingaben entgegenzunehmen, den Zug zusammen mit dem
Brett dem Zugprüfer zu übergeben. Wenn der Zugprüfer sein ok
gibt, dann wird der Zug der Zugliste übergeben, die ihn speichert.
Danach wird besagter Zug dem Brett übergeben, das ihn ausführt
und das geänderte Brett der Anzeige übergeben. Bei speziellen
Eingaben (undo) wird die Zugliste um den letzten Zug befragt,
der Zug auf rückgängig umgeformt und ebenfalls wieder dem
Brett übergeben, dass dan brav den Zug rückgängig macht.
Ha. Da haben wir schon den ersten Designfehler. Wenn ein
Zug ausgeführt wird, dann kann es sein, dass eine Figur
geschlagen wird. Diese Figur verlässt dann das Brett und
taucht nicht mehr auf. Allerdings: Um den Zug rückgängig machen
zu können, ist es daher notwendig, daß die geschlagene Figur
beim Zug, in dem sie geschlagen wurde, zu speichern.
Aber was solls. Fangen wir mal an:
Wir haben ein Brett:
class Brett
{
public:
Figur Apply( Zug& zug );
Figur Piece( Position& pos );
void Init();
protected:
Figur Inhalt[8][8];
};
Apply führt einen Zug aus und liefert die Figur die gschlagen
wurde, wenn überhaupt.
Piece liefert die Figur, die auf einem Feld steht.
Init hingegen stellt die Figuren zum ersten mal auf.
Also machen wir gleich mal weiter mit einer Position. Das
ist leicht:
class Position
{
public:
Position( int z, int s ) : Zeile(z), Spalte(s) {}
int Zeile;
int Spalte;
bool IsValid();
};
An dieser Stelle verlasse ich das Dogma ein klein wenig.
Zeile und Spalte sind public Variablen. Eigentlich möchte
man sowas nicht haben. Allerdings gibt es immer wieder solche
Klassen, die eigentlich eher Datenhaltecharakter haben. Bei
solchen Klassen verlasse ich auch schon mal das Dogma.
To be continued ...
Was ist eine Figur?
Nun eine Figur besteht aus 2 Informationen. Welcher Figurtyp
und welche Farbe die Figur hat.
enum FTyp = { Keine, Bauer, Turm, Laeufer, Springer, Dame, Koenig };
enum FFarbe = { weiss, schwarz };
class Figur
{
public:
Figur( FTyp& t, FFarbe& f ) : Typ( t ), Farbe( f ) {}
FTyp Typ;
FFarbe Farbe;
};
Damit ergibt sich aber auch sofort, wie ein Zug aussieht:
class Zug
{
public:
Zug( const Position& von, const Position& nach,
Figur& f );
void WurdeGeschlagen( const Figur& f );
protected:
Position Von;
Position Nach;
Figur ZugFigur;
Figur Geschlagen;
};
.....
Bitte erspar mir jetzt das weitere tippen. Das geht jetzt
eine ganze Weile so dahin. Jede Klasse hat eine definierte
Aufgabe. Jede Klasse kann sich dabei auch auf andere Klassen
stützen. Zb. Wird die Ausgabefunktion der 'Anzeige' das
Brett Feld für Feld durchgehen und die dort befindlichen
Figuren abfragen. Je nach Figur wird dann die Ausgabe gemacht.
zb. so
void Anzeige::Show( const Brett& Aktuell )
{
for( int Zeile = 0; Zeile < 8; ++Zeile )
for( int Spalte = 0; Spalte < 8; ++ Spalte ) {
ShowFigur( Brett.Piece( Position( Zeile, Spalte ) ) );
}
cout << endl;
}
}
void Anzeige::ShowFigur( const Figur& f )
{
if( Figur.Typ != Keine ) {
if( Figur.Farbe == schwarz )
ShowFigurBlack( f );
else
ShowFigurWhite( f );
}
else
cout << " ";
}
void Anzeige::ShowFigurBlack( const Figur& f )
{
char Symbols[] = " btlsdk";
cout << Symbols[ f ];
}
void Anzeige::ShowFigurWhite( const Figur& f )
{
char Symbols[] = " BTLSDK";
cout << Symbols[ f ];
}
Jede Funktion für sich ist trivial und leicht zu durchschauen.
Und doch bilden sie ein Geflecht, so dass sie in der Spiel
Klasse leicht zu verwenden ist:
class Spiel
{
...
protected:
Brett m_Brett;
Anzeige m_Anzeige;
Zugliste m_Zuege;
Zugchecker m_Checker;
};
bool Spiel::Do( const Position& von, const Position& nach )
{
Figur wer = m_Brett.Piece( von );
Zug* pZug = new Zug( von, nach, wer );
if( m_Checker.IsValid( pZug ) ) {
Figur Geschlagen = m_Brett.Apply( pZug );
pZug->Geschlagen = Geschlagen;
m_Zuege.Add( pZug );
}
else {
delete pZug;
return false;
}
m_Anzeige.Show( m_Brett );
return true;
}
Auch diese Funktion ist im Grunde schon selbsterklärend.
Sie ist kurz genug um überschaubar zu bleiben und beim
Durchlesen der Funktion offenbart sich relativ schnell
was diese Funktion macht und wie sie es macht. Wichtig:
Für verschíedene Dinge, macht sich die Funktion nicht
selbst die Hände schmutzig, sondern delegiert. Die Abfrage
welche Figur an der von-Position sitzt, die überlässt
sie dem Brett. Ob ein Zug gültig ist oder nicht, das will
diese Funktion vom Zugprüfer wissen. Diese Funktion führt
auch den Zug nicht selbst aus; das wird wiederum an das
Brett delegiert. Und schliesslich die Anzeige: Die wird
wiederum an das Anzeige Objekt abgetreten. Möchte ich
eine andere Form der Anzeige haben, sei es ein schönes
graphisches Display oder vielleicht überhaupt ein reales
Schachbrett an dem ein Roboter echte Figuren verschiebt:
Dafür zuständig ist das Anzeige Objekt. Das kann ich
jederzeit gegen ein anderes tauschen. Das kümmert zb.
den Zugprüfer oder die Zugliste überhaupt nicht. Daher
kann auch eine Änderung im Anzeigeobjekt die Zugprüfung
nicht beeinflussen bzw. mir dort einen Fehler hineinhauen.