Mühle Objektorientiert

Hallöle,
ich versuche immer, möglichst objektorientiert zu arbeiten und scheitere dennoch immer an meinen eigenen Konzeptverstrickungen. Deshalb möchte ich gerne eine kleine Diskussion über das sinnvollste Konzept starten, wie man ein Mühlespiel mit 0 bis 2 Computergegnern erarbeitet. Dies soll eher auf theoretischer als auf codereicher Ebene geschehen.
0 bis 2 Gegner meint, dass man entweder zu zweit als Menschen spielen, zwei Computern beim Spielen zusehen oder gegen einen Computer spielen kann.

Ich möchte einmal meine unmöglich umsetzbare Idee offenbaren, welche mich dazu brachte, dieses Thema zu eröffnen:
Ich habe einen ViewController, das ist die Startklasse, der Controller, der View und Data verwalten kann. Ja, es handelt sich um eine iOS-App (nicht weiter relevant, das geht ja bei jeder anderen Plattform so), der eine oder andere hat es vielleicht am Namen dieser Klasse erkannt. Nun hatte ich mir für die Daten folgendes überlegt:
[ul]
[li]Mill (eng. Mühle) ist die Klasse, welche alle anderen Datenklassen enthält. Das sind also ein Player-Array, das Feld und möglicherweise Infos, wer am Zug ist, usw.
[/li][li]Field enthält einen Array aus allen Feldern, die verfügbar sind. Wie man das bei nem Mühlefeld clever in ein- oder mehrdimensionale Array packt, das ist ja eher trivial, sei hier mal nicht diskutiert.
[/li][li]Player ist die Spielerklasse, die Methoden wie das Setzen von Zügen (Mühle hat drei Phasen, Steinsetzen, Steinverschieben und Springen) enthält. Ferner gibt es ja noch statische Informationen wie den Namen, die Anzahl verfügbarer Steine und derartigen Krimskrams.
[/li][li]KI erbt vom Player und hat im Prinzip also die gleichen Methoden. Das heißt es ist auch Teil des Player-Arrays, da es sich streng genommen um einen solchen handelt. Es enthält zusätzlich das KI-“Gehirn”, also komplexe Methoden zum Nachdenken, was man denn setzen könnte.
[/li][/ul]

So und hier bin ich auch schon beim Problem angelangt. Die Methoden zum Setzen von Zügen können gar nicht im Player drin sein, da es zum Beispiel gar nicht auf das Feld zugreifen kann, da Player und Field ja in Mill initialisiert wurden. Diese Methoden könnten im Notfall ja auch in Mill direkt drin sein, aber dann ist die Auslagerung auch nicht ziemlich konsequent.
Das größere Problem ist hier die KI. Sie hat ja die gleichen Probleme. Sie kann nicht auf das Feld zugreifen.

Wie würdet ihr das lösen? Ich möchte nicht, dass am Ende wieder alles nur in der Hauptklasse herumliegt, das ist ja nicht der Sinn der Sache.
Schöne Grüße
FranzFerdinand

dass ein Player die Mill kennt und
mill.setField(x,y);
aufruft oder auch mit Field als separates Objekt ähnliches, ist keine große Sache

dass KI von Player erbt klingt dagegen teils merkwürdig aber mag für manches wie ‚Anzahl verfügbarer Steine und derartigen Krimskrams‘ nötig sein,
Methoden weniger, je nachdem was benötigt ist, was in Mill/ Field zentraler steht usw.,

statt Erben ja auch standardmäßig an Komposition, an Zugriff auf ein bestimmtes Player-Objekt für die KI, zu denken


meine Philosophie dazu mal wieder:
keine Angst vor ‚alles nur in der Hauptklasse‘, kein zwanghaft schönes Programmiermodell,
außer vielleicht falls Hauptzweck der Übung wirklich die Übung des Modells statt Programmieren des Spiels ist,

schlecht aufgeräumte Klassen bemerkt man normalerweise auch so wenn sie auftreten und Refactoring dann ziemlich klar,
es braucht schon viel bis man sich wirklich verbaut hat, Methoden und kleine Konzepte sind oft zu jedem Zeitpunkt noch schnell zu verschieben,
dabei dann idealerweise auch gut gesehen/ gelernt was warum falsch war und woanders besser passt


wenn ohne richtige Vorstellung/ Erfahrung etwas kompliziertes verfolgt dann die ganze Zeit Zweifel (wie schon im ersten Posting)
und gut möglich dass genauso in einer schlechten Ecke landend, die besser wieder Umbau erfordert,

im Worst Case alles noch ohne was zu lernen, ohne richtig zu wissen wo wann was warum doch etwas falsch gemacht,

aber das ist nun auch schon negativ zugespitzt während das andere positiv dargestellt :wink:

Erstmal kurz: Das Ding heißt nicht „Mill“, sondern Morris: Nine men's morris - Wikipedia

Ansonsten finde ich (aus meiner bisherigen Erfahrung), dass da eine Klasse fehlt, die (falls ich das richtig verstanden habe) viele deiner Fragen beantworten bzw. Probleme lösen würde:

Move

Eine Repräsentation eines Zuges. Wegen der drei Phasen, die du angedeutet hast, ist die Frage, wie man die Klasse modelliert, vielleicht nicht ganz so trivial, wie in anderen Fällen. Aber ich denke, dass man die Züge der drei Phasen relativ leicht UND „sauber“ in einer „Move“-Klasse unterbringen kann: Sowohl Steinsetzen, Steinverschieben als auch Springen kann man abbilden durch eine Move-Instanz, die erstmal NUR das Quell- und das Zielfeld enthalten muss. (Beim Steinsetzen ist das Quellfeld dann „null“). (Man könnte noch überlegen, ob man den ausführenden Spieler oder die Steinfarbe da reinpackt. Aber es kann gut sein, dass man die gar nicht braucht. Und wenn man sie nicht braucht, packt man sie eben auch nicht rein…).

So eine Klasse hätte aus OO-Sicht mehrere Vorteile. Wenn ich das richtig verstanden habe, soll „Field“ ja das komplette Spielfeld sein (man könnte es auch „Board“ nennen … andernfalls wäre die Frage, wie man die einzelnen Felder nennt - „Position“? Die in einem Array zu speichern ist übrigens ein Implementierungsdetail, das man nach außen hin NICHT sehen sollte).

Die Methoden zum Setzen von Zügen können gar nicht im Player drin sein, da es zum Beispiel gar nicht auf das Feld zugreifen kann, da Player und Field ja in Mill initialisiert wurden.

Ich finde die Frage nach der Initialierung da nicht so relevant. Man KÖNNTE ja einfach hindengeln:

class Field {
    void setPiece(int x, int y, Piece piece) { ... }
}

class Mill {
    void init() {
        Player player = new Player();
        Field field = new Field();

        player.setField(field); // There you got it....
   }
}

Also einfach per Setter eine Referenz reinpacken und gut. Alternativ könnte man dem Player in seine „makeMove“-Funktion das Brett einfach mit übergeben. Aber gerade aus dieser etwas abstrahierend-idealisierenden Sicht ist das eben IMHO NICHT gut.

Ich denke, eine entscheidende Frage ist: WER hat die Kompetenz, das Spielfeld zu verändern?

Einerseits bin ich ja oft für „direkte“ Modellierungen. Man macht das, was eben Sache ist, und wenn das die Realität gut beschreibt, ist das auch OK. Aber bei so einem Spiel könnte man dann ja sowas machen wie

class CheatingPlayer extends Player {
    @Override
    public void makeMove(Field field) {
        field.setPiece(0,0,mine);
        field.setPiece(0,1,mine);
        field.setPiece(0,2,mine); // Nye, nye, nye....
    }
}

Zu diesem Fall, ein paar Gedanken:

  • Sowohl „Player“ als auch „Field“ sollten vermutlich interfaces sein. Der Punkt „KI erbt von Player“ ist damit so gesehen abgehandelt
  • Der Player bekommt nur ein „Field“ zu sehen, das er NICHT verändern kann. Es könnte also sowas geben wie
interface Field {
    Piece getPieceAt(int x, int y) { ... }
}
interface MutableField extends Field {
    void setPieceAt(int x, int y, Piece piece) { ... }
}
  • Der Player führt keine Züge direkt aus, sondern erstellt nur „Move“-Objekte. Diese Moves tatsächlich auszuführen obliegt der übergeordneten, gottgleichen Instanz, die auch schreibenden Zugriff auf’s Field hat, und sicherstellt, dass die Züge gültig sind, und sicherstellt, dass die Player abwechselnd dran sind.

Wichtiger Punkt: So eine Move-Klasse hätte noch einen weiteren, GANZ entscheidenden Vorteil: Man braucht sie für die KI sowieso! Zumindest die „klassischen“ KIs, mit Minimax und ggf. AlphaBeta-Pruning erfordern, in der abstraktesten Form, genau DREI Dinge:

  • Sie müssen einen Zug machen können
  • Sie müssen die Spielsituation bewerten können (mit einer Art „Punktzahl“, die besagt, wie gut die Situation für einen Spieler ist, bzw. ob ein Spieler gewonnen hat)
  • Sie müssen einen Zug rückgängig machen können

Intern wird die KI dann z.B. eine Liste von möglichen Zügen berechnen und davon den besten auswählen. Die KI könnte in dem Sinne dann ein spezieller Spieler sein, dem man „vertraut“, und der auch Züge ausführen darf, aber das sind Details. Jedenfalls würde sie den besten Zug zurückgeben, und „Das Spiel“ könnte den Zug dann ausführen.

Zuerst stimm ich mal Marco13 in fast allen Punkten zu :wink: Uebrigens ist das ein Command Pattern.

Aber ich haette da noch ne Frage:

Was meinst du damit genau?

Das ist genau der Ort, wo die Logikmethoden hingehoeren (egal ob mit oder ohne Move Klasse). Wobei ich die Klasse spontan World genannt haette.

Zusaetzlich wuerde ich noch den Gedanken verfolgen, ob es sich lohnt, aus der Field-Klasse (unbedingt in Board umbenennen) eine immutable Klasse zu machen. Dann wird es (spaeter) evtl. (kenn mich mit iOS nich wirklich aus) einfacher, eine Board-Instanz an den Renderthread zu uebergeben und zeichnen zu lassen, ohne die Logikberechnung zu blockieren. Ausserdem wird der KI Code wahrscheinlich einfacher (bzw eh mit Kopien arbeiten).

Aus der genannten Quelle:

Nine Men’s Morris is a strategy board game for two players dating at least to the Roman Empire.[1] The game is also known as Nine Man Morris, Mill, Mills, The Mill Game, Merels, Merrills, Merelles, Marelles, Morelles and Ninepenny Marl[2] in English.

Also ich würde - was die Player angeht - gar nicht so viele Playerklassen erstellen, sondern genau nur eine. Diese bekäme dann (ich nenne sie mal) Think- oder Turn-Objekte (enum; HUMAN, KI) welche dann entweder auf eine Eingabe warten oder den KI-Algo ausführen.
Das “Board” (“Field” ist tatsächlich etwas ungünstig, es sei denn die Spielsteine sähen wie Rugby-Spieler aus :D) kann man dann dem Player-Objekt per “makeMove(Board b)” übergeben, diese reicht es an das Turn-Instanz weiter und die Turn-Instanz ruft dann “.move(Field from, Field to)” auf (Hier sieht man, warum “Field” als Name für das ganze Spielbrett ungünstig ist - Ein Spielbrett besteht meist aus Feldern, die man besetzen kann).

Zur Unterscheidung der beiden Phasen, würde ich den Spielern ruhig ihren Stapel mit Steinen mit implementieren. Die Phasen lassen sich dann leicht dadurch bestimmen, ob noch Steine auf diesem Stapel sind.

Dann wäre die Quelle für einen Setz-Zug halt der Stapel.

Um solche Züge Rückgängig zu machen, müssten auch die Stapel zugänglich sein. Somit gehören sie vielleicht eher zum Board? Ein weißer und ein schwarzer Stapel.

Abstrahieren lässt sich der Stapel sicher durch die Anzahl der Steine auf dem Stapel, aber das ist letztlich ein Implementierungsdetail.

Wie meinst Du das? Bezieht sich das auf diesen „Rückaufruf“, den ich meinte, dass ein Player in Mill erstellt wird und dann die Mill kennt? Wie das funktionieren soll, verstehe ich eben weiterhin nicht. Außer man legt die ActionListener in der Mill ab und ruft darin eine Methode von Player auf, indem man die wichtigen Daten von Mill übergibt. So würde ich das machen. Meinst Du das oder ist das eher doppelt gemacht, wenn man da schon vorhandene Daten unschön nochmal übergibt?
[/QUOTE]
dort am Anfang dürfte es wirklich darum gegangen sein dass ein Player Mill kennen können sollte,
was dagegen spricht erschließt sich mir nicht, zumindest nicht aus Java-Sicht unter normalen Objekten, jeder kann jeden kennen

wenn Mill etwa einen Player erzeugt, dann gibt Mill sich selber als Parameter im Konstruktor mit: new Player(this) und schon kennt Player Mill,
oder später set-Methode, init-Methode usw.

wenn ich mal gegen Parameter-Übergabe bin, dann vor allem aus Anzahl-Sicht :wink: :
unschön wäre es bei 30 Spielzügen 30x etwas zu übergeben wie später „makeMove(Board b)“ genannt wurde,
lieber nur einmal wichtige Objekte überall übergeben, obwohl beides eine Methode und in etwa gleich viel Code ist


aber ich glaube es gibt hier längst ganz andere Dinge zu thematisieren, schwierig nun die Antworten von vielen einzusammeln, meins wohl unwichtig,
und dein Posting aktuell habe ich auch nicht komplett gelesen :wink:

Ich habe es zwar gelesen, muss/müßte aber genauer nachdenken bzw. es mit dem Code matchen (den ich noch NICHT gelesen habe).

Aber was mir bei einem Blick auf die „Move“-Klasse aufgefallen ist: Das, was ich damit ursprünglich meinte, war nichts, was eine bestimmte „Funktionalität“ hätte. Ich meinte wirklich nur die Repräsentation eines Zuges als Objekt. In Java also wirklich nur

class Move {
    Point source;
    Point target;
}

Darüber hinaus gehende Funktionalitäten wären eher woanders verortet. Natürlich ist es leicht, bestimmte idealisierte Wunschvorstellungen als Pseudocode hinzuschreiben, aber … ich würde zumindest versuchen, irgendwann irgendwo Code zu haben, der sich wie eine kleine Geschichte liest

while (!gameOver()) {
    Move move = activePlayer.getMoveFor(board);
    if (!isValid(move)) informPlayerAboutBad(move);
    else {
        board.applyMove(move);
        changeActivePlayer();
    }
}

Aber man kann sicher überlegen, ob irgendwelche anderen Strukturen Sinn machen.

Den Einlass von @Spacerat verstehe ich nicht ganz…:

Der entsprechende Code…


    init(pl1isKI: Bool, pl2isKI: Bool, name1: String, name2: String) {
        if pl1isKI {
            players.append(Player(name: name1, num: 1, type: .Player))
        } else {
            players.append(Player(name: name1, num: 1, type: .KI))
        }
        
        if pl2isKI {
            players.append(Player(name: name2, num: 2, type: .Player))
        } else {
            players.append(Player(name: name1, num: 1, type: .KI))
        }
    }

… gefällt mir persönlich nicht so. Ich finde, dass es sich gerade beim Player anbieten würde, dedizierte Klassen zu erstellen, die ein gemeinsames interface implementieren:

interface Player {
    Move getMoveFor(Board board);
}

// Die KI-Instanzen werden vom Spiel selbst erstellt, und dürfen 
// z.B. auch Zugiff auf "Mill" (das Spiel) haben usw....
class KI implements Player {

    private Mill mill;

    @Override
    Move getMoveFor(Board board) { ... }
}

// Die Human-Implementierung braucht irgendwelches
// absurdes GUI-Zeug, was mit dem "Kern" des Spiels
// (als "Engine") nichts zu tun hat
class Human implements Player {

    private JComponent whereTheUserWillClickToMove;
    private ActionListener someActionListenerCouldBeHereAsWell;
    ...

    @Override
    Move getMoveFor(Board board) { ... }
}


// Mal einen Schritt hinausgedacht, über "KI oder Human" ;-)
class NetworkPlayer implements Player {

    private Socket socket; 
    ...

    @Override
    Move getMoveFor(Board board) { ... }
}

Das entscheidende ist, dass es für das Spiel selbst ja keine Rolle spielt (oder spielen sollte), welche ART ein Spieler ist. Der Spieler hat nur brav seine Züge abzuliefern. Und obiger Code könnte dann (übersetzt auf Java, sorry…) zu sowas werden wie

    void init(Player player0, Player player1) {
        this.players[0] = player0;
        this.players[1] = player1;
    }

// Aufruf:

mill.init(new Human(), new KI()); 
//oder mill.init(new Human(), new Human()); 
//oder mill.init(new KI(), new KI()); 
//oder ...

Ja, wenn ich das Element für Menschliche Spieler auch Player genannt hätte, würde ich es auch nicht verstehen. Glücklicherweise aber habe ich besagtes Element HUMAN genannt. :wink:
Die Init-Methode sähe dann so aus:

  players.append(new Player(name1, turnType1);
  players.append(new Player(name2, turnType2);
}```
Das Ganze kann man natürlich auch noch um das Element NETWORK erweitern und man muss dabei nichts weiter tun, als genau nur dieses Enum zu implementieren, weil von der einzig existierenden Player-Klasse ja genau darauf delegiert wird. Hauptsache ist doch, die verschiedenen Zug-Logiken, die mehr oder weniger komplex sein können, belasten nicht die Lesbarkeit der Player-Klasse, dessen Logik ja sonst für alle Player gleich bleibt (oder nicht? Schiebung!!!). ;)

@Spacerat , @Marco13 : Ihr seit ja gar nict so weit auseinander…

Die Frage ist doch: gibt es Logik für den Spieler, die für alle Spielertypen immer gleich ist (außer auf die Spezialisierungslogik zu delegieren)?

Wenn diese Frage mit Nein beantwortet wird ist das Enum von Spacerat genau das Spieler-Interface mit seinen unterschiedlichen implementierungen, die Marco sich wünscht.

bye
TT

Ohja, da hatte ich das mit dem “TurnType” wohl nicht richtig erfasst. Dass man als (ggf. sogar bessere) Alternative zum “klassischen”

interface Player { Move doMove(); }

class AbstractPlayer implements Player {
    // Lotsa common stuff
    ...

    // ("doMove" still abstract)
}

class Human extends AbstractPlayer {
    Move doMove() { /* Finally implemented here */
}

die Zugerzeugng noch weiter rausziehen kann (im Sinne des Mantras “Composition over Inheritance”)


class Player {
    // Lotsa common stuff
    ...
    TurnType turnType;
    Move doMove() { turnType.generateMove(); }

}
class HumanTurnType implements TurnType {
    Move generateMove() { /* Finally implemented here */
}

stimmt natürlich (die Frage, ob Enum oder nicht, ist da schon fast ein Detail. Ich nutze Enums eher zurückhaltend für solche Sachen)

[QUOTE=Marco13]die Frage, ob Enum oder nicht, ist da schon fast ein Detail. Ich nutze Enums eher zurückhaltend für solche Sachen[/QUOTE]Enum deswegen, weil man damit wunderbar auf einfachste Weise Choices füllen kann - man hat alle Implementationen des (äh…) Interfaces stets als Set/Liste zur Hand aus welcher man auswählen kann. Außerdem funktioniert auch switch damit viel besser - das ist hilfreich, weil man damit im Programmablauf (z.B. bei Weltraumsimulationen InSector/OutOfSector) Schritte bei der Berechnung überspringen kann, wenn sie nicht benötigt werden.

Das führt jetzt vielleicht etwas zu weit in Details, die für Swift ja gar nicht mehr relevant sind. Aber noch kurz: Ein switch ist eben oft etwas, was mit Polymorphie besser abgebildet werden kann - deswegen kann man den “Vorteil” von Enums dafür wohl in Frage stellen.

Hallöle,

ich danke euch allen für eure Antworten.
Zum Thema Move-Klasse:
@Marco13 Ah, ich habe es verstanden! Das soll nicht als Container sondern nur als Angabe dienen, was passiert ist. Dann kann ich in der Hauptklasse einen Move-Array einbauen, wo dann alle bisherigen Züge eingespeichert sind. Das hat den Vorteil, dass man Züge rückgängig machen kann und beim Laden eines Gespeicherten Spiels nur alle Züge nochmal ausführen muss, die gespeichert sind.

Ich habe das nun so implementiert:

class Move {
    var oldPosition : (Int, Int)
    var newPosition : (Int, Int)
    
    init(oldPosition : (Int, Int), newPosition : (Int, Int)) {
        self.oldPosition = oldPosition
        self.newPosition = newPosition
    }
}

Und in der Hauptklasse existiert der Array var moves = [Move](), der immer mit neuen Zügen befüllt wird, in den entsprechenden Move-Methoden, wo auch immer ich sie hinpacke und wie sie aussehen werden.

Zum Thema der Spieler:
In der Tat hat Swift eine Möglichkeit für Interfaces, die sich hier Protokolle nennen. Einschränkungen bezüglich der Funktionalität müssen wir hier nicht machen, ich nehme liebend gerne Javavorschläge und Beispiele entgegen, ist ja ein Javaforum. :slight_smile: Ich programmiere nur im Hintergrund in Swift. Mir geht es auch nicht um Code, sondern Ideen der Modellierung.

So sehen die Spieler nun aus:

protocol Player {
    var num : Int {get}
    var name : String {get}
    
    var stoneDepot : Int {get set}
    var stonesOnField : Int {get set}
    var couldJump: Bool {get}
    
    init(name: String, num: Int)
}

class Human : Player {
    var num : Int
    var name : String
    
    var stoneDepot = 9
    var stonesOnField = 0
    var couldJump: Bool {
        return stonesOnField == 3 && stoneDepot == 0 ? true : false
    }
    
    required init(name: String, num: Int) {
        self.num = num
        self.name = name
    }
}

class KI : Player {
    var num : Int
    var name : String
    
    var stoneDepot = 9
    var stonesOnField = 0
    var couldJump: Bool {
        return stonesOnField == 3 && stoneDepot == 0 ? true : false
    }
    
    required init(name: String, num: Int) {
        self.num = num
        self.name = name
    }
}

Die Schnittstellen mit der Spielerinteraktion sind ja nicht weiter relevant, die existieren dann aber auch. :wink: Ich muss dann nur noch überlegen, wie ich das mit der KI löse, wie ich ihr zugriff verschaffe und WO ich sie denken lasse.

Zwei Sachen verstehe ich noch nicht so recht aus den Kommentaren zuvor:
Was ist mit TurnType und Move getMoveFor(Board board) { ... } gemeint?

PS: Hat noch jemand Ideen wegen der Sache mit den gültigen Zügen und der Aufteilung des Spielbretts? Mir fällt da einfach nichts effektives zu ein.

Schöne Grüße
FranzFerdinand

PS: Den Code auf GitHub hab ich aktualisiert.

Die Frage nach der Repräsentation des Brettes ist ganz interessant. Eine (damit zusammenhängende, aber nicht notwendigerweise entscheidende) Frage ist, wie denn die Felder addressiert werden sollen. Bisher scheint ein Feld ja durch zwei ints repräsentiert zu sein, und das Brett ist ein Array mit 3 Spalten und 8 Zeilen. Man könnte natürlich ganz ““verschwenderisch”” einen 7x7-Array nehmen, ähnlich wie beim Bild auf Wikipedia angedeutet (da als 1…7 und a…g). (Ich dachte auch noch kurz an eine Art “Graph”, aber das bringt für die Adressierung erstmal nichts - und der Vorteil, den es in der Verschiebe-Phase hätte, wäre vernachlässigbar)

[quote=FranzFerdinand]

class Move {
 var oldPosition : (Int, Int) 
  var newPosition : (Int, Int)
[...]

[/quote]Darf ich da mal kezerisch fragen, warum die Felder hier keine Objekte sind?

class Field {
  var pos : (int, int)
  var neigbour: Map<Direction,Field> // als von der Idee her...
}

class Move {
 var from :Field
 vat to: Field
// ...
}

bye
TT

*** Edit ***

[quote=Marco13;137960]Ein switch ist eben oft etwas, was mit Polymorphie besser abgebildet werden kann - deswegen kann man den “Vorteil” von Enums dafür wohl in Frage stellen.[/quote]Manchmal sind Enums aber auch wirklich Praktisch:```enum Direction{
NORTH,EAST,SOUTH,WEST
}

class Filed{
private final Map<Direction,Field> neigbours = new HashMap<>();

public void setNeighBour(Direction d, Field f) { neigbours.put(d,f); }

public Collection getPossibleDirections(){
Collection possibleDirections = new HashSet<>();
for(Direction d: Direction.values()) {
Field field= neigbours.getOrDefault(d, Field.OFF_MAP);
if(field.isFree())
possibleDirections.add(d);
}
return possibleDirections;
}


Das Feld interessiert sich jetzt gar nicht mehr für die konkrete Richtung und auch nicht, ob es eine Nachbarn in dieser Richtung überhaupt hat. Das kann alles transparent von außen gesteuert werden.

Wenn man später mal mehr Richtungen bekommt muss man nur mehr EnumKonstanten hinzufügen...

bye
TT

@Marco13 Auch das ist möglich und wahrscheinlich noch einfacher als das Gedöns, was ich als aktuelle Idee habe.

@Timothy_Truckle Ich bin immer wieder äußerst begeistert von Deinem Ansatz, krampfhaft alles zu objektorientieren. Diesmal find ich die Idee auch richtig knorke. Warum ich aus dem Int-Tuple ein Objekt Field machen soll, war mir nicht klar, aber als ich dann den Code darunter als Beispiel sah, fand ich es sehr gelungen.

Aber um auf das eigentliche zurückzukommen: Muss ich dann nicht trotzdem eine ewige Auflistung von Nachbarzuordnungen machen, wie befürchtet? Dadurch hab ich mit schicker aussehendem Code (und effizienter) das gleiche Problem:

...//Initialisierung der Felder irgendwo

void zuordnung() {
   fields[0].setNeighBour(Direction.EAST, 0);
   fields[0].setNeighBour(Direction.SOUTH, 0);
   fields[1].setNeighBour(Direction.WEST, 1);
   fields[1].setNeighBour(Direction.EAST, 1);
   fields[1].setNeighBour(Direction.SOUTH, 1);
}

Oder hab ich den Code unten falsch verstanden?

Anstelle von 2 Dimensionen kann man bei Mühle auch 3 Dimensionen für ein Feld verwenden.

Schaut man das Spielbrett an so sind es drei ineinander geschachtelte Quadrate. Dafür könnte man die erste Dimension wählen. Für die anderen bieden Dimensionen dann die x und y Achsen.

Damit könnte man auch recht einfach die Nachbarschaftsbeziehungen bestimmen.

Eine Verschiebe Aktion, ist dann auch nur legal, wenn sich eine Dimension ändert. Entweder horizontal oder vertikal oder auf ein anderes Quadrat. Wobei bei letzterem das ganze nur legal ist, wenn sich horizontaler oder vertikaler Wert in der Mitte befinden.
Horizontales verschieben nur wenn vertikaler Wert nicht mittig ist.
Vertikales verschieben nur wenn horizontaler Wert nicht mittig ist.

Das „setNeighBour“ war wohl ein Tippfehler von „setNeighbour“…

Ansonsten ist das mit den Enums schon sehr Java-Spezifisch. Die Frage, wie wahrscheinlich es ist, dass „eine Richtung dazukommt“ könnte man zwar stellen, aber sie wirkt bei Mühle im ersten Moment sehr gekünstelt… bis man sich fragt, ob man Morabaraba - Wikipedia auch mal implementieren will :smiley:

Das hängt auch mit der Recht interessanten Sichtweise zusammen, das ganze als 3D-Würfel anzusehen. Bei obigem ist das noch deutlicher, aber beim „normalen“ Mühle sind die Nachbarschaften an den Ecken ja in diesem Sinne nicht vollständig.

Solchen Code wie

void zuordnung() {
   fields[0].setNeighBour(Direction.EAST, 0);
   fields[0].setNeighBour(Direction.SOUTH, 0);
   fields[1].setNeighBour(Direction.WEST, 1);
   fields[1].setNeighBour(Direction.EAST, 1);
   fields[1].setNeighBour(Direction.SOUTH, 1);
}

finde ich auch nicht „schön“ (insbesondere, da er ja noch VIEL länger und repetitiver wäre). Und wie schon angedeutet: Ich sehe keinen sooo großen Nutzen darin, die Nachbarschaftsbeziehungen als solche direkt zu speichern. Man könnte das auch auf eine (vermutlich recht überschaubare) Funktion

boolean areNeighbors(Field f0, Field f1) {...}

zurückführen. Aber das sind irgendwie drei Fragestellungen/Ansätze, die sich teilweise überlappen:

  • Man könnte alles in einen 7*7-Array packen, ohne explizite Nachbarschaftsinformation
  • Man könnte in jedem Feld die Nachbarn speichern, und die händisch verdrahten, wie oben beschrieben
  • Man könnte in jedem Feld die Nachbarn speichern, aber die Verdrahtung auf Basis einer „isNeighbor“-Funktion automatisch machen
  • Man könnte alles in einen 7*7-Array packen, und eine getNeighbors-Funktion anbieten, die sich die Nachbarn bei Bedarf zusammensucht
    Um da irgendwelche Empfehlungen geben zu können, müßte ich aber erst nochmal genauer überlegen, wie man die Nachbarschaften gut beschreiben kann (also welche Array-Elemente Nachbarn sind, und ob man das leicht in eine „isNeighbor“-Funktion packen könnte)