Tic Tac Toe Architektur



  • Schönen guten Morgen

    Und zwar habe ich ein anliegen. Ich würde mich gerne in Sachen Softwarearchitektur verbessern. Dazu entwickel ich grade ein Tic Tac Toe. Leider habe ich aber immernoch das gefühl, dass mein Design nicht das beste ist. Ich würde mich freuen, wenn hier der ein oder andere seine perfekte Lösung zur Verfügung stellen würde.

    Meine Lösung für das Backend gibt es natürlich auch. Bin noch nicht sicher, dass sie richtig funktioniert, aber darum soll es ja auch nicht gehen.

    class Player
        {
            public String Name { get; set; }
            public GameScore Score { get; set; }
    
            public Player(String name)
            {
                this.Name = name; 
            }
        }
    
    class GameScore
        {
            public int Wins { get; set; }
            public int Defeats { get; set; }
            public int Pats { get; set; }
        }
    
    class GameBoard
        {
            public Player[,] GameBoardArray { get; private set; }
    
            public GameBoard()
            {
                this.GameBoardArray = new Player[3, 3];
            }
            public GameBoard(int rowsAndColums)
            {
                this.GameBoardArray = new Player[rowsAndColums,rowsAndColums];
            }
    
            public void insertMoveInGameBoard(Player player, Point position)
            {
                this.GameBoardArray[position.X, position.Y] = player;
            }
        }
    
    class GameController
        {
            private Player[] Player;
            private GameBoard Board;
            private GameVerifier Verifier;
            private int currentPlayer = 0;
            private Boolean isGameOver = false;
    
            public GameController(String nameOfPlayer1, String nameOfPlayer2, int boardSize)
            {
                this.Player = new Player[2];
                this.Player[0] = new Player(nameOfPlayer1);
                this.Player[1] = new Player(nameOfPlayer2);
    
                this.Board = new GameBoard(boardSize);
    
                this.Verifier = new GameVerifier();
    
                Random random = new Random();
                this.currentPlayer = random.Next(0, 2);
            }
    
            public void Turn(Player player, Point point)
            {
                if (isGameOver)
                {
                    MessageBox.Show("Das Spiel ist um!");
                    return;
                }
                if (player != this.Player[this.currentPlayer])
                {
                    MessageBox.Show("Du bist nicht dran!");
                    return;
                }
                if(this.Verifier.IsMoveAllowed(this.Board, point))
                {
                    MessageBox.Show("Dieser zu ist nicht möglich.");
                    return;
                }
    
                this.Board.insertMoveInGameBoard(player, point);
    
                if (this.Verifier.IsGameOver(this.Board))
                {
                    this.isGameOver = true;
                    this.Player[this.currentPlayer].Score.Wins++;
                    this.Player[1-this.currentPlayer].Score.Defeats++;
                    MessageBox.Show("Gewonnen!");
                    return;
                }
    
                if (this.Verifier.isPat(this.Board))
                {
                    this.isGameOver = true;
                    this.Player[this.currentPlayer].Score.Pats++;
                    this.Player[1 - this.currentPlayer].Score.Pats++;
                    MessageBox.Show("Keiner hat gewonnen!");
                    return;
                }
                this.currentPlayer = 1 - this.currentPlayer;
            }
        }
    
    class GameVerifier
        {
            public Boolean IsGameOver(GameBoard board)
            {
                for (int columnAndRowNumber = 0; columnAndRowNumber < 3; columnAndRowNumber++)
                {
                    if (isRowComplete(columnAndRowNumber, board) || isColumnComplete(columnAndRowNumber, board))
                    {
                        return true;
                    }
                }
    
                return isDiagonalComplete(board);
            }
            public Boolean isPat(GameBoard board)
            {
                if (this.IsGameOver(board))
                {
                    return false; 
                }
    
                for (int row = 0; row < 3; row++)
                {
                    for (int column = 0; column < 3; column++)
                    {
                        if (board.GameBoardArray[row, column] == null)
                        {
                            return false;
                        }
                    }
                }
                return true;
            }
    
            public Boolean IsMoveAllowed(GameBoard board, Point atPosition)
            {
                int boardSize = board.GameBoardArray.GetLength(0);
    
                if (boardSize < atPosition.X || boardSize < atPosition.Y)
                    return false;
    
                if (atPosition.X < 0 || atPosition.Y < 0)
                    return false;
    
               return (board.GameBoardArray[atPosition.X, atPosition.Y] == null);
            }
    
            private Boolean isRowComplete(int rowNumber, GameBoard inGameBoard)
            {
                if (inGameBoard.GameBoardArray[rowNumber, 0] == inGameBoard.GameBoardArray[rowNumber, 1] && inGameBoard.GameBoardArray[rowNumber, 1] == inGameBoard.GameBoardArray[rowNumber, 2] && inGameBoard.GameBoardArray[rowNumber, 0] != null)
                    return true;
                return false;
            }
    
            private Boolean isColumnComplete(int columnNumber, GameBoard inGameBoard)
            {
                if (inGameBoard.GameBoardArray[0, columnNumber] == inGameBoard.GameBoardArray[1, columnNumber] && inGameBoard.GameBoardArray[1, columnNumber] == inGameBoard.GameBoardArray[2, columnNumber] && inGameBoard.GameBoardArray[0, columnNumber] != null)
                    return true;
                return false;
            }
    
            private Boolean isDiagonalComplete(GameBoard inGameBoard)
            {
                if (inGameBoard.GameBoardArray[0, 0] == inGameBoard.GameBoardArray[1, 1] && inGameBoard.GameBoardArray[1, 1] == inGameBoard.GameBoardArray[2, 2] && inGameBoard.GameBoardArray[0, 0] != null)
                    return true;
                if (inGameBoard.GameBoardArray[0, 2] == inGameBoard.GameBoardArray[1, 1] && inGameBoard.GameBoardArray[1, 1] == inGameBoard.GameBoardArray[2, 0] && inGameBoard.GameBoardArray[0, 2] != null)
                    return true;
                return false;
            }
        }
    

    Nun, wie wärt ihr die Sache angegangen?

    Beste Grüße
    Tapio

    PS: Gerne natürlich auch in anderen Sprachen. In der Hoffnung, dass ich dann dennoch durchblicke 🙂


  • Mod

    Tapio schrieb:

    Nun, wie wärt ihr die Sache angegangen?

    Ich habe aufgehört zu lesen, als du dein Spielfeld aus Spielern zusammen gesetzt hast.



  • Okay. Wieso ist das eine schlechte Idee? Und wie hättest du es gemacht?
    Stelle hier ja die Frage, damit ich es in der Zukunft besser machen kann.



  • Aber so ist es bis zu 9 Spieler multiplayerfähig! ^^
    (Du sollst wahrscheinlich eine Spielfeld-Klasse bauen die wenn überhaupt jeweils eine Referenz auf den Spieler enthält wenn dieser sein Kreuz setzt. Und Spieler-Instanzen gibt es dann wie in der Realität genau zwei)



  • Aber so ist es bis zu 9 Spieler multiplayerfähig! ^^
    (Du sollst wahrscheinlich eine Spielfeld-Klasse bauen die wenn überhaupt jeweils eine Referenz auf den Spieler enthält wenn dieser sein Kreuz setzt. Und Spieler-Instanzen gibt es dann wie in der Realität genau zwei)

    Das ist doch schon so. Nur dass die Spielfeld-Klasse nichts anderes macht als zu einer Position einen Spieler zu referenzieren. Den Spieler muss ich ja identifizieren. Sonst weiß ich nicht welcher gewonnen hat.



  • Irgendwie recht kompliziert für so ein kleines Spiel.
    Mach dir ein 2d Array welches du mit 0, 1, 2 befüllst. Oder eben einem enum Wert {leer, spieler1, spieler2}
    Von mir aus auch noch gepackt in eine Klasse mit passenden Settern/Gettern, sodass du komfortabel den Zustand an Stelle {x,y} bekommst.

    Und dann schreibst du dir ein paar Funktionen, die auf diesem Spielfeld Operationen ausführen:
    - Kreuz/Kreis (bzw. Spieler1/Spieler2) setzen
    - prüfen, ob jemand gewonnen hat {SpielNichtFertig, Spieler1Gewonnen, Spieler2Gewonnen, Unentschieden}
    - KI für einen der Spieler implementieren
    ...

    Keep it simple!


  • Mod

    Tapio schrieb:

    Aber so ist es bis zu 9 Spieler multiplayerfähig! ^^
    (Du sollst wahrscheinlich eine Spielfeld-Klasse bauen die wenn überhaupt jeweils eine Referenz auf den Spieler enthält wenn dieser sein Kreuz setzt. Und Spieler-Instanzen gibt es dann wie in der Realität genau zwei)

    Das ist doch schon so. Nur dass die Spielfeld-Klasse nichts anderes macht als zu einer Position einen Spieler zu referenzieren. Den Spieler muss ich ja identifizieren. Sonst weiß ich nicht welcher gewonnen hat.

    Das ist nicht so. Bei dir besteht das Spielfeld aus Spielern (und übrigens auch unbesetzte Felder, was ist mit denen?). Wenn solch ein krasses Missverhältnis zwischen Modell und Realität vorliegt, dann ist höchst wahrscheinlich (und hier mit Sicherheit) ein schlechtes Modell vor.



  • SeppJ schrieb:

    Das ist nicht so.

    Aber wie ist es denn? Und vor allem, wie wäre es besser?

    SeppJ schrieb:

    Bei dir besteht das Spielfeld aus Spielern (und übrigens auch unbesetzte Felder, was ist mit denen?)

    Naja, mein Spielfeld ist erstmal leer wenn das Array erstellt wird. Alle Felder sind null, also nicht gesetzt. Setze ich nun einen Spieler, wird eine Referenz in dem Feld des Arrays hinterlegt. Das heißt, am Ende zeigen alle Felder des Spieler 1 auf das gleiche Spieler-Objekt. So jedenfalls mein Verständnis.

    SeppJ schrieb:

    Wenn solch ein krasses Missverhältnis zwischen Modell und Realität vorliegt, dann ist höchst wahrscheinlich (und hier mit Sicherheit) ein schlechtes Modell vor.

    Die Frage wäre dann, wie wäre es besser? Mache ich ein Symbol-Objekt, welches den Spieler kennt, und platziere dieses auf dem Gameboard? Und bin ich dann weiter?
    Gefühlt würde ich sagen, es ist das gleiche, nur durch die Brust ins Auge.



  • Hab deinen Beitrag mal zum Anlass genommen ein TicTacToe in C zu schreiben:

    #include <stdio.h>
    #include <stdlib.h>
    
    int field[3][3];
    
    void print_field(void);
    int player_won(void);
    void flush(void);
    
    int main(void)
    {
    
        int i,j;
        int status;
        int insert[2];
    
        for (i=0;i<3;i++)
            for (j=0;j<3;j++)
                field[i][j] = 0;
    
        while (1)
        {
            print_field();
            for (i=1;i<3;i++)
            {
                printf("Spieler %d (y,x)",i);
                scanf("%1d,%1d",&insert[0],&insert[1]);
                flush();
                if ((insert[0] < 1 || insert[0] > 3) || (insert[1] < 1 || insert[1] > 3))
                {
                    puts("Ungueltige Eingabe!");
                    i--;
                    continue;
                }
                if (field[insert[0] - 1][insert[1] - 1] != 0)
                {
                    puts("Ungueltige Eingabe!");
                    i--;
                    continue;
                }
                field[insert[0] - 1][insert[1] - 1] = i;
                status = player_won();
                switch (status)
                {
                    case 0:
                        puts("Unentschieden!");
                        getchar();
                        return 0;
                    case 1:
                        puts("Spieler 1 hat gewonnen!");
                        getchar();
                        return 0;
                    case 2:
                        puts("Spieler 2 hat gewonnen!");
                        getchar();
                        return 0;
                }
                print_field();
            }
        }
        return 0;
    }
    
    void print_field(void)
    {
        int i,j;
        system("cls");
        for (i=0;i<3;i++)
        {
            for (j=0;j<3;j++)
                printf("%d",field[i][j]);
            printf("\n");
        }
        return;
    }
    
    int player_won(void)
    {
        int i,j;
        for (i=1;i<3;i++)
            for (j=0;j<3;j++)
                if (field[j][0] == i && field[j][1] == i && field[j][2] == i)
                    return i;
        for (i=1;i<3;i++)
            for (j=0;j<3;j++)
                if (field[0][j] == i && field[1][j] == i && field[2][j] == i)
                    return i;
        for (i=1;i<3;i++)
        {
            if (field[0][0] == i && field[1][1] == i && field[2][2] == i)
                return i;
            if (field[2][0] == i && field[1][1] == i && field[0][2] == i)
                return i;
        }
        for (i=0;i<3;i++)
            for (j=0;j<3;j++)
                if (field[i][j] == 0)
                    return 3;
        return 0; /* Unentschieden */
    }
    
    void flush(void)
    {
        int c;
        while ((c = getchar()) != EOF && c != '\n');
    }
    

    Was ich noch ändern würde ist die Benutzereingabe, hatte mir überlegt mit 1-9 ein Feld zu wählen,
    allerdings ist solch ein switch-case Konstrukt zu umständlich. Vielleicht fällt einem was besseres ein.
    Perfekt ist das aber nicht, da verlangst du zu viel 🙂



  • Hallo,

    eventuell solltest du noch eine Klasse Field hinzufügen, aus dieser setzt du dann das Spielfeld zusammen. Also statt aus Spielern, aus einzelnen Feldern.

    Das Feld darf nun meinetwegen wissen, welcher Spieler es belegt hat. Also etwas in der Richtung:

    class GameBoardField
    {
        // definiert ob ein das feld von einem
        // spieler ausgewählt wurde
        public bool IsEmpty { get; set; }
        public Player Player { get; set; }
    }
    

    Und in deinem GameBoard anschließend:

    class GameBoard
    {
        public GameBoardField[,] GameBoardArray { get; private set; }
    
        public GameBoard()
        {
            this.GameBoardArray = new GameBoardField[3, 3];
        }
    
        public GameBoard(int rowsAndColums)
        {
            this.GameBoardArray = new GameBoardField[rowsAndColums,rowsAndColums];
        }
    
        public void insertMoveInGameBoard(Player player, Point position)
        {
            GameBoardField field = this.GameBoardArray[position.X, position.Y];
            if(field.IsEmpty)
            {
               field.Player = player;
               field.IsEmpty = false;
            }
        }
    }
    


  • @Bitmapper
    Was deine Lösung angeht, kann ich noch nicht so ganz einen Transfer ziehen zu meiner Lösung. Was ist an deiner Lösung besser? Und warum? Ich möchte ja davon lernen.

    @inflames2k
    Finde ich eine gute Idee. Das macht Sinn für mich. Werde das adaptieren.

    Was ist denn noch Verbesserungswürdig? Ich meine SeppJ hat ja gesagt, dass es so kacke ist, dass sich das Weiterlesen nicht lohnt. Leider hat er nicht gesagt warum und es nur an einer Zeile fest gemacht. Meist sind solche Menschen Trolle, aber da er Moderator ist, denke ich sollte seinem Urteil zu trauen sein. Würde mich entsprechend über mehr Kritik freuen und vor allem Verbesserungsvorschläge.



  • Naja, ich habe es so ähnlich gemacht wie c++ progger vorgeschlagen hat. Dass meine Lösung besser ist kann ich ja damit begründen,
    dass SeppJ sie noch nicht bemängelt hat 🙂



  • Hallo Bitmapper,

    zwischen deiner Implementierung und der von Tapio gibt es einen riesen Unterschied. - Schon allein, weil du in C geschrieben hast.

    Hallo Tapio,

    der Aussage von SeppJ kannst du ruhig trauen. Selbst wenn er kein Moderator wäre ist es ein berechtigter Einwand auf deine Logik.

    Ein Spielfeld besteht nun mal nicht aus den Spielern. Diese bewegen sich nur darauf.



  • inflames2k schrieb:

    Hallo Bitmapper,

    zwischen deiner Implementierung und der von Tapio gibt es einen riesen Unterschied. - Schon allein, weil du in C geschrieben hast.

    Hallo Tapio,

    der Aussage von SeppJ kannst du ruhig trauen. Selbst wenn er kein Moderator wäre ist es ein berechtigter Einwand auf deine Logik.

    Ein Spielfeld besteht nun mal nicht aus den Spielern. Diese bewegen sich nur darauf.

    Das sehe ich auch ein. Es hörte sich aber eher so an, als wäre da bedeutend mehr schlecht, als nur das Gameboard.


  • Mod

    Tapio schrieb:

    Das sehe ich auch ein. Es hörte sich aber eher so an, als wäre da bedeutend mehr schlecht, als nur das Gameboard.

    Nein, ich habe tatsächlich an der Stelle aufgehört zu lesen. Das ist ein Designfehler, der muss korrigiert werden. Sich anzusehen, was du im Rest des Programms mit diesem Design gemacht hast (oder eher: Wie um diesen Fehler herum arbeitest), lohnt daher die Mühe nicht. Beim Querlesen des Rests (so viel habe ich dann doch noch gemacht) fiel mir bloß auf, dass es ungeheuer kompliziert aussah für solch ein Spielchen. Diese Kritik hatte c++ progger aber schon geäußert, daher habe ich es nicht wiederholt.



  • Tapio schrieb:

    @Bitmapper
    Was deine Lösung angeht, kann ich noch nicht so ganz einen Transfer ziehen zu meiner Lösung. Was ist an deiner Lösung besser? Und warum? Ich möchte ja davon lernen.

    Nicht besser.

    Tapio schrieb:

    Was ist denn noch Verbesserungswürdig?

    Plane, ein 4-Gewinnt draus zu machen. Schau, wieviel Code verändert werden muss.
    GameScore bleibt. Na, so Mini-Sachen will ich mal nicht zählen.

    Player bleibt, aber nur weil Player zu dumm ist. Das Benutzerinterface und die KI gehören in die Players, finde ich. Da darfste mal vererben.

    GameBoard bleibt fast. Aber es ist mir zu dumm. Es kann den GameVeryfier ruhig integrieren.

    GameController hat keinen Schimmer, ob da jetzt zwei Schachspieler an einem Schachbrett sitzen oder zwei 4-Gewinnt-Spieler vor einem 4-Gewinnt-Zettel. Das ist gut.

    In die Impelentierungsdetails gehe ich noch nicht, da ist noch recht viel.


Log in to reply