[linux-l] Designfrage bzgl. Deckverwaltung

Steffen Dettmer steffen at dett.de
Sa Nov 5 04:12:15 CET 2005


* Kai Kuehne wrote on Fri, Oct 21, 2005 at 12:25 +0200:
> Undzwar habe ich vor mir fuer das Kartenspiel Magic eine kleine
> Deckverwaltung (bis jetzt in C++) zu basteln.

Ich hab den Thread gelesen, aber irgendwie bin ich immer noch nicht so
viel schlauer, worum's beim Magic geht. IMHO ist das elementares Wissen
für ein Design. Erstmal braucht man ja nur einen Bleistift :)

> Eine Karte hat z.B. eine Eigenschaft "Farbe".
> Moegliche Werte sind schwarz, blau, rot... usw.
> Es kommen keine neuen dazu.

Wie schon von anderen geschrieben, riecht das erstmal nach enum. Nicht
nach #define, weil die nicht typ-sicher und immer global sind:

#define ROT 0
#define BLAU 1
#define LINKS 0
#define RECHTS 1
Farbe = LINKS;

namespace outerspace {
   int ROT; // knallt
};

bei enum merkt das der Compiler :) Defines haben aber andere Vorteile.
Beispielsweise kann man die Listen über mehrere Module definieren (also
z.B. in einem extra Modul plötzlich ein LILA definieren). 

Wenn's viele sind, würde ich enums per source-code Generierung erzeugen,
kriegt man ja mit einer Handvoll Perlzeilen hin. Machen wir auf Arbeit
öfter mal, macht sich gut, schon ab 20 wenn man auch wert->string
braucht (für Debug und sowas).

> Wie wuerdest ihr das in C++ nun machen?
> Mir fallen folgende Moeglichkeiten ein:
> 
> 1) Ein enum pro Eigenschaft der Karte
> 2) Eine "CardProperty" Klasse mit - z.B. einem const vector fuer
> moeglichen Werte

Ist schon wieder zu flexibel; dann kann eine Karte ja rot und blau sein,
was vermutlich im Spiel aber so nicht geht. Dann wäre IMHO das Design
falsch. Enum sind immer gut, wenn sich Sachen gegenseitig ausschliessen
und ungeordnet (!) sind. Mit enums sollte man nicht rechen und so (auch
wenns natürlich alles geht). Man kann natürlich entsprechende Operatoren
definieren, macht sich manchmal bei Vergleichen gut. Wenn man richtig
rechnen will (Beispiel Skat: Ass sind 11, kommt nach der 10, As plus
Bube (2) sind definiert 13 usw), möchte man wohl keinen enum sondern
lieber "static consts" (billige Singletons). "static consts" sind IMHO
in C++ immer noch besser als #defines, die man eigentlich nur für C
braucht.

Leider kann man enums nicht iterieren.

Für Skat würde ich wohl die Skatwerte verwenden, obwohl hier der
Nachteil ist, dass der Compiler beispielsweise keine unvollständigen
"switches" anmeckert, was man vielleicht gut gebrauchen kann.

Vielleicht

// SkatKarte wäre JavaStyle, skat_karte eher C++ STL Style

class skat_karte : public karte {
  friend class skat_spiel;
  public:
    typedef int farbe;
    static const farbe Karo  = 9;
    static const farbe Herz  = 10;
    static const farbe Pik   = 11;
    static const farbe Kreuz = 12;

    farbe
    get_farbe(void) const;

  protected:

  private:
    // Konstruktion ist verboten. Es gibt immer genau 32 Karten.
    //   (aber das Deck macht das einmal)
    skat_karte(farbe, blatt);

    // Kopieren ist verboten. Es gibt immer genau 32 Karten.
    skat_karte(const skat_karte&);

    // Destruktion ist verboten. Es gibt 32.
    virtual ~skat_karte(void);

    // Assignment ist verboten.
    skat_karte&
    operator=(const skat_karte&);

  private:
    // Die farbe einer Karte ändert sich nie
    const farbe farbe_;
    const blatt blatt_; // Ass, Koenig, 7 usw.
};

Damit geht dann
if (karte1.get_farbe() > karte2.get_farbe())
oder

spielwert = spiel * skat_karte::Karo;

skat_karte::Karo ist natürlich schon wieder nicht ideal, weil Karo ja
ein Spiel ist, na ja :)

Beim Blatt (oder wie das heisst) funktioniert das nicht mehr, klar, die
Luschen.

Verwenden dürfte man dann nur Referenzen (die viel mächtiger als
gleichnamige Java-Konstrukte sind, die in C++ eher Zeigern nahekommen).
Zeiger braucht man in C, in C++ kann man mit Referenzen viel mehr
reissen. Wenn man fast nur noch Referenzen benutzt, beginnt man den
Unterschied zu C zu verstehen ;)

Dann müsste man das Deck aufbauen:

// C++ hat keine nativen Listen. Daher hier C. Ist prakmatisch.
//   Die Konstruktion hier ist ne Schnellgeburt und bestimmt falsch.
typedef int blatt;
const blatt Ass = 11;
const blatt Zehn = 11;
const blatt Koenig = 4;
// usw.
const blatt blaetter[] = {
	Ass,
	Zehn,
	Koenig,
	// usw.
};

Dann ein Spiel bauen:

// "const std::vector<skat_karte> skat_spiel;" wäre schön, aber lässt sich
//    nicht richtig konstruieren - oder?

class skat_spiel : private std::vector<skat_karte> {
  public:
    skat_spiel(void);
    ~skat_spiel(void);

    // Karten ändern sich ja nie:
    const skat_karte&
    operator[](int karten_nummer);
  
    // Zufallsziehung
    const skat_karte&
    get_irgendeine(void);

    // und was man so braucht
    
  private:
    skat_spiel *instance_;
};

skat_spiel::skat_spiel(void)
{
   for (farbe f = Karo; farbe <= Kreuz; farbe++) {
      for (int blatt_nummer = 0; 
           blatt_nummer < sizeof(blaetter)/sizeof(blaetter[0]);
           blatt_nummer++
      ) {
         push_back(skat_karte(f, blaetter[blatt_nummer]));
      }
   }
   assert(size() == 31);
}

Den vector skat_spiel kann man jetzt nicht mischen oder sonstwie kaputtkriegen
(wenn ich nix vergessen hab).

Man kann man aber sowas machen:

// wir haben hier erstmal nur ein einziges Spiel:
static const skat_spiel das_eine_spiel;

// sollen 32 const-Referenzen sein. Bin mir nicht sicher, ob die decl so
//   OK ist :)
typdef const skat_karte& karte_ref;
karte_ref stapel[32];

Dann einfach die Karten der Reihe nach zu nehmen (wie in
das_eine_spiel):

for (int kartenNummer = 0; kartenNummer < 32; kartenNummer++) {
   stapel[kartenNummer] = das_eine_spiel[kartenNummer];
}

und dann einfach per Zufall mischen.

mit den const skat_karte& kann man dann arbeiten (Farbe prüfen etc).
Karten entstehen und verschwinden dabei nicht.

Das hat den Nachteil, dass hier immer noch zwei Eigentümer sein könnten,
die diese Karte haben.

In der Praxis (macht man das natürlich viel einfacher, klar :) ) könnte
man "wo_bin_ich" als Karteneingenschaft definieren, und mit normalen
Referenzen arbeiten, wobei es halt sowieso keine setter für Farbe etc.
gibt (wie ja auch im Beispiel nicht).


> 3) Was anderes, was mir noch nicht eingefallen ist..

Aus'm Bauch herraus würde ich erstmal über der Regel "Mache
Datenstrukturen ruhig komplex, aber den Code einfach" meditieren, oft
fällt einem dann was ein. Dann einen prototypen bauen. Und dann das
Design vereinfachen. Wenn es partou nicht mehr einfacher geht, ist es
fertig :)

> Bei 1) bekomme ich Zahlen, was imho ziemlich unuebersichtlich wird,
> wenn jede Eigenschaft der Karten-Klasse eine Ganzzahl ist.

Trading-Card-Game? Kann man da konstante Decks bauen? Kenn das halt
nicht. Jedenfalls würde ich mich über etwas wie "setFarbe()" sehr
wundern, weil sich Karten selbst nicht ändern sollten. Aber vielleicht
gibt's ein "binVerzaubert()" oder sowas, klar.

> Danke fuer Tipps und Ideen.

Na ja, schlimmstenfalls hab ich Dich jetzt noch mehr verwirrt :)

Jedenfalls bekommt man es in C++ dann so hin, dass nichts geht, was man
nicht darf (also z.B. /kann/ man halt keine Karte kopieren. Wenn sie im
"skat" liegt, ist sie "const" und /kann/ sich nicht ändern usw). Das ist
IMHO eine grosse Stärke - das geht in Java z.B. nicht so gut.

Das ist IMHO eine prima Übung. Ich hab ein paar Datenstrukturen, die
änliche Eigenschaften haben (unkopierbare Objekte [unkopierbar, weil mit
resourcen verknüpft] in Listen, alle anderen arbeiten mit Zeigern oder
natürlich den geliebten Referenzen - letztere können ja nie NULL werden
:)). Vorher war da was kompliziertes, was aber in wenigen Zeilen C++ mit
STL listen ausgedrückt war. Funktionierte, aber man konnte kaum was
anfassen (warten). Beim Versuch unittests zu schreiben, war's dann
vorbei, weil die Implementierung halt nicht sicherstellte, dass alles
richtig sein muss, sondern dann man es so und so aufruft und hier und
dass in der Reihenfolge etc. Ich hab dann winzige billig Objekte
definiret, die den unkopierbaren verknüpft sind, die kann man dann
sortieren und so weiter. Ist IMHO schon ähnlich zu so einem Skatspiel.
Bloss dass es kein Herz hat :-)

oki,

Steffen

-- 
Dieses Schreiben wurde maschinell erstellt,
es trägt daher weder Unterschrift noch Siegel.



Mehr Informationen über die Mailingliste linux-l