TicTacToe



  • @Leon0402 sagte in TicTacToe:

    Interessieren würde ich mich, ob templates nach wie vor möglich sind, wenn man eine Auswahl stellt bei Benutzereingabe. Daher z.B. 3x3, 4x4, 5x5 ... müsste doch theoretisch gehen mit ner switch oder?

    Templateparameter müssen Compiletimekonstanten sein.



  • Achso, wegen der Abfrage ob ein Feld schon besetzt ist... Das wird ausgeschlossen bei
    case action::logIn. Da steht, dass die Aktion nur ausgeführt wird, wenn Feld == '0'.

    Ansonsten vielen Dank für euer Feedback. Ich schraube heute nacht nach der Arbeit und morgen vormittag mal dran herum und versuche die meisten Punkte zu erledigen. Auch was @Schlangenmensch geschrieben hat mit der Indexfunktion versuche ich umzusetzen. Mal sehen was dabei rauskommt 😅.



  • Warum werden hier templates vorgeschlagen? Das schaut mir nach einem Use-Case aus, wo ich nie auf die Idee kommen würde, ein Template zu nehmen. Das muss man echt nicht übertreiben.



  • Es war im Zusammenhang mir std:arrray. Da aber gute Argumente für einen vector geliefert worden sind, ist kein Template notwendig/sinnvoll.



  • Ich mag deinen Datentypen char für das Spielfeld nicht.
    Das kann man sicherlich lesbarer machen, z.B. als enum oder mit Pointern auf Player.
    Wobei Player dann eine class ist, was vielleicht auch sinnvoll wäre.

    Ich finde, man kann deinen Code zwar recht schnell erfassen, ist aber auch ein Miniprogramm. Bei wachsender Größe würden ein paar Abstraktionen mehr, dem Code sehr gut tuen.



  • Guten Morgen. Ein paar Dinge konnte ich letzte Nacht noch umschreiben. Zwei Dinge aber noch.

    Zum einen hast du @Leon0402 mir Beispiele geschrieben wie ich meine print-Funktion kürzen kann. Allerdings sehe ich da bei dir keinen Zeilenumbruch?

    Zum anderen hast du etwas geschrieben von "Tastatur fällt ja weg", "das Ui", "Später kannst du das dann gegen eine Maus austauschen". Ich glaube du hast mein Vorhaben etwas falsch interpretiert. Es gibt kein UI und die Tastatursteuerung soll bleiben. Das Spiel findet auf einer LED Matrix statt (https://www.youtube.com/watch?v=D_QBlFIQk-o). Und zwar sieht ein Spielzug dann so aus:

    Der Spieler 1 beginnt. Oben links in der Ecke (wie jetzt auch) leuchtet eine Zelle weiß, der Rest ist aus. Nun will der Spieler seine Auswahl z.B. nach rechts verschieben. Dafür drückt er die Pfeiltaste nach rechts. Der Spieler, oder besser gesagt der Client, schickt nun nur noch die Information "Pfeil rechts wurde gedrückt" zum Server. Dieser schaut, ob der Spieler überhaupt dran ist und verwaltet eigentlich das Spielfeld etc. intern. Der Server stimmt zu, weil Spieler 1 an der Reihe ist und generiert aus dem jetzt geänderten Spielfeld ein TPM2 Datenpacket, welches er zur LED-Matrix schickt. Also Client sendet eigentlich nur Tasten, Server verwaltet komplettes Spiel und die LED-Matrix stellt das ganze dar. Es wird dann beim Client sowie beim Server jeweils eine Netzwerkklasse geben und der Server bekommt noch eine extra Klasse (vielleicht auch nur eine Funktion), welche das Protokoll zusammenbaut. Ich habe also nur die Lampe hier in der Konsole dargestellt. Man kann sich vorstellen: '0' = aus, '1' = rot, '2' = blau und '*' = weiß.

    Weil ich gerade den Beitrag von @Jockelx gelesen habe. Später gibts auch keine chars mehr. Daraus wird später enum class mit der entsprechenden Farbe. Wie schon geschrieben sind hier die Konsolenausgaben (und auch die chars, welche die Farben symbolisieren) nur temporär und fliegen raus.

    So, nun zu meinen Änderungen. Ich habe einige angesprochene Dinge geändert. Als nächstes wären die Funktionen zum Auswerten des Gewinners dran. Ich könnte diese jetzt umschreiben, dass sie mit @Schlangenmensch 's Koordinaten (x,y) funktionieren, aber du hattest ja geschrieben, das könne man noch schneller machen. Wie würden die Funktionen bei dir aussehen @Leon0402 ? Windows.h hab ich drinnen gelassen. Die brauche ich zum Tastatur auslesen. Später wird ein Makro das System unterscheiden und die richtige Funktion für das Betriebssystem einsetzten.

    Hier der aktuelle Code:

    main.cpp

    #include <iostream>
    #include <vector>
    #include <Windows.h>
    #include <thread>
    #include "field.hpp"
    
    
    
    void procedure(Field &myfield, action userAction) {
    	system("cls"); //Konsole leeren
    	myfield.doAction(userAction);
    	myfield.printField();
    	std::this_thread::sleep_for(std::chrono::milliseconds(100)); //Verzögerung, damit Tasten nicht immer wieder erneut eingelesen werden
    }
    
    
    
    int main() {
    
    
    	Field myfield(8, 6, 4); //Breite Spielfeld, Höhe Spielfeld, Länge der Reihe für Gewinn (toWin)
    	myfield.printField();
    
    	while (true) {
    
    
    		if (GetAsyncKeyState(VK_LEFT)) {
    			procedure(myfield, action::moveLeft);
    		}
    
    		else if (GetAsyncKeyState(VK_RIGHT)) {
    			procedure(myfield, action::moveRight);
    		}
    
    		else if (GetAsyncKeyState(VK_UP)) {
    			procedure(myfield, action::moveUp);
    		}
    
    		else if (GetAsyncKeyState(VK_DOWN)) {
    			procedure(myfield, action::moveDown);
    		}
    
    		else if (GetAsyncKeyState(VK_RETURN)) {
    			procedure(myfield, action::logIn);
    			if (myfield.isWinner()) {
    				std::cout << "Congratulation! Player " << myfield.player << " won the Game!!!" << std::endl;
    				break;
    			}
    		}
    	}
    
    	std::this_thread::sleep_for(std::chrono::seconds(3));
    	system("cls");
    }
    

    field.hpp

    #ifndef FIELD_HPP_INCLUDED
    #define FIELD_HPP_INCLUDED
    
    #include <vector>
    #include <utility>
    
    enum class action { moveLeft, moveRight, moveUp, moveDown, logIn };
    
    class Field {
    
    public:
    	Field(unsigned int width, unsigned int height, unsigned int toWin);
    	void printField() const;
    	bool isPosFree();
    	void doAction(action selectedAction);
    	char player;
    	bool isWinner();
    
    private:
    	unsigned int coordToIndex(unsigned int x, unsigned int y);
    	void moveField(unsigned int x, unsigned int y);
    	void changePlayer();
    	bool calculateVertical();
    	bool calculateHorizontal();
    	bool calculateDiagonalDown();
    	bool calculateDiagonalUp();
    	
    	std::vector<char> fieldVec;
    	char cursor = '*';
    	char clipboard;
    	std::pair<unsigned int, unsigned int> actPos;
    	unsigned int width;
    	unsigned int height;
    	unsigned int toWin;
    };
    
    #endif
    

    field.cpp

    #include <vector>
    #include <iostream>
    #include <algorithm>
    #include "field.hpp"
    
    
    
    Field::Field(unsigned int width, unsigned int height, unsigned int toWin)
    	: width(width), height(height), toWin(toWin) {
    	fieldVec.assign((width * height), '0');
    	player = '1';
    	actPos = { 0, 0 };
    	clipboard = fieldVec[coordToIndex(actPos.first,actPos.second)];
    	fieldVec[coordToIndex(actPos.first, actPos.second)] = cursor;
    }
    
    
    
    unsigned int Field::coordToIndex(unsigned int x, unsigned int y) {
    	return y * width + x;
    }
    
    
    
    void Field::printField() const {
    	int index = 0;
    	for (unsigned int i = 0; i < height; i++) {
    		for (unsigned int j = 0; j < width; j++) {
    			std::cout << fieldVec[index] << " ";
    			index++;
    		}
    		std::cout << std::endl;
    	}
    }
    
    
    
    bool Field::isPosFree() {
    	return clipboard == '0';
    }
    
    
    
    void Field::doAction(action selectedAction) {
    	switch (selectedAction) {
    
    		case action::moveLeft: {
    			if (actPos.first > 0)
    				moveField(actPos.first - 1, actPos.second);
    			break;
    		}
    
    		case action::moveRight: {
    			if (actPos.first < (width - 1))
    				moveField(actPos.first + 1, actPos.second);
    			break;
    		}
    
    		case action::moveUp: {
    			if (actPos.second > 0)
    				moveField(actPos.first, actPos.second - 1);
    			break;
    		}
    
    		case action::moveDown: {
    			if (actPos.second < height - 1)
    				moveField(actPos.first, actPos.second + 1);
    			break;
    		}
    
    		case action::logIn: {
    			if (isPosFree()) {
    				fieldVec[coordToIndex(actPos.first, actPos.second)] = player;
    				clipboard = player;
    			}
    			else {
    				std::cout << "Position ist belegt!" << std::endl << std::endl;
    			}
    			break;
    		}
    
    		default: {
    			std::cout << "Error: Selected Action out of range!";
    			break;
    		}
    
    	}
    }
    
    
    
    void Field::moveField(unsigned int x, unsigned int y) {
    	std::pair<int, int> newPos = { x, y };
    	fieldVec[coordToIndex(actPos.first, actPos.second)] = clipboard;
    	actPos = newPos;
    	clipboard = fieldVec[coordToIndex(actPos.first, actPos.second)];
    	fieldVec[coordToIndex(actPos.first, actPos.second)] = cursor;
    }
    
    
    
    void Field::changePlayer() {
    	if (player == '1')
    		player = '2';
    	else
    		player = '1';
    }
    
    
    
    bool Field::isWinner() {
    	
    	if (calculateVertical() || calculateHorizontal() || calculateDiagonalDown() || calculateDiagonalUp()) {
    		return true;
    	}
    	else {
    		changePlayer();
    		return false;
    	}
    }
    
    
    
    bool Field::calculateVertical() {
    	int corr = 0;
    	for (unsigned int j = 0; j < width; j++) {
    		for (unsigned int i = j; i < fieldVec.size(); i += width) {
    
    			if (fieldVec[i] != player)
    				corr = 0;
    			if (fieldVec[i] == player)
    				corr++;
    			if (corr == toWin)
    				return true;
    		}
    		corr = 0;
    	}
    
    	return false;
    }
    
    
    
    bool Field::calculateHorizontal() {
    	int corr = 0;
    	for (unsigned int i = 0; i < fieldVec.size(); i++) {
    
    		if ((i % width == 0) || (fieldVec[i] != player))
    			corr = 0;
    		if (fieldVec[i] == player)
    			corr++;
    		if (corr == toWin)
    			return true;
    	}
    	
    	return false;
    }
    
    
    
    bool Field::calculateDiagonalDown() {
    	int corr = 0;
    	for (unsigned int j = 0; j < fieldVec.size(); j++) {
    		for (unsigned int i = j; i < fieldVec.size(); i += (width + 1)) {
    
    			if ((i % width == 0) && (j != i))
    				break;
    
    			if (fieldVec[i] == player)
    				corr++;
    			if (corr == toWin)
    				return true;
    			if (fieldVec[i] != player)
    				corr = 0;
    		}
    		corr = 0;
    	}
    
    	return false;
    }
    
    
    
    bool Field::calculateDiagonalUp() {
    	int corr = 0;
    	for (int j = (width * height) - 1; j >= 0; j--) {
    		for (int i = j; i >= 0; i -= (width - 1)) {
    
    			if ((i % width == 0) && (j != i))
    				break;
    
    			if (fieldVec[i] == player)
    				corr++;
    			if (corr == toWin)
    				return true;
    			if (fieldVec[i] != player)
    				corr = 0;
    		}
    	}
    
    	return false;
    }
    


  • Mach mal das auf jeden Fall noch:

    @Leon0402 sagte in TicTacToe:

    Würdest du so eine Methode aber brauchen, solltest du eine Referenz zurück geben und die Methode als const kennzeichnen.

    const std::vector<char> getFieldVec();
    

    Getter sind idR const und der Rückgabewert ist idR const& (bei nicht primitiven Datentypen).
    Eine Kopie als const zurück zugeben macht jedenfalls gar keinen Sinn.



  • Hab sie gleich komplett gelöscht. Die Methode war wohl noch vom Anfang des Projekts^^. Aktuell wir sie nirgends verwendet. Aber danke nochmal für die Erinnerung 👍🏻



  • @Jockelx sagte in TicTacToe:

    Ich mag deinen Datentypen char für das Spielfeld nicht.
    Das kann man sicherlich lesbarer machen, z.B. als enum oder mit Pointern auf Player.
    Wobei Player dann eine class ist, was vielleicht auch sinnvoll wäre.

    Bei einem enum player_t { PLAYER_ONE = 'X', PLAYER_TWO = 'O', PLAYER_NONE = ' ' }; bin ich noch dabei. Aber eine Klasse für die beiden Spieler und dann noch Pointer darauf im Spielfeld? Overengineering?



  • @Swordfish
    Ja, kann gut sein, dass das hier noch too much ist. Habe auch extra sehr defensiv den Vorschlag gemacht, da ich die Notwendigkeit auch nicht so 100%ig sehe.
    Spätestens sobald aber einer der Player AI sein kann, wird es Zeit für mehr Abstraktion. Aber wenn das jetzt schon quasi fertig ist, dann hast du sicherlich Recht.



  • @Jockelx sagte in TicTacToe:

    Spätestens sobald aber einer der Player AI sein kann, wird es Zeit für mehr Abstraktion.

    Weiß nicht, ob ich da übereinstimme*. Man braucht doch nur eine Funktion estimateBestMove(currentPosition) (wobei currentPosition natürlich auch enthält, wer dran ist). Und darin generiert man Züge und macht einen MinMax-Algorithmus. Fertig. Sehe nicht, dass man da irgendwie im Spielfeld irgendwas Komplizierteres benötigt. Halte es einfach!

    Edit: * = weiß ich doch. Und zwar stimme ich nicht überein.
    Edit2: bei TTT kann man statt mit MinMax einfach alles durchprobieren.



  • @wob sagte in TicTacToe:

    Man braucht doch nur eine Funktion estimateBestMove(currentPosition)

    Was hat das denn jetzt mit einer Player-Klasse zu tun, die sinnvoll ist (oder vielleicht halt auch nicht).

    Schon in der main würde ich mir die Finger brechen, wenn ich das mit char hier und char da machen müsste, statt irgendwie sowas

    int main() {
    	Player player[2] = {Human, Ai};
            int round = 0;
    	while(! player[round%2].isWinning())
    	{
                player[round%2].makeTurn(); // estimateBestMove or userinput
                ++round;
    	}
    	return 0;
    }
    


  • @Jockelx sagte in TicTacToe:

    Eine Kopie als const zurück zugeben macht jedenfalls gar keinen Sinn.

    Bist du dir da ganz sicher?



  • @Mechanics sagte in TicTacToe:

    Bist du dir da ganz sicher?

    Trolling?

    Wenn nicht, was findest Du besser?

    const int foo() { return 42; }
    

    oder

    const const const /* <~ just to make really, really sure */ int foo() { return 42; }
    

    ?

    ...

    // ...
    int bar{ foo() };
    bar = 47;  // uups
    // ...
    


  • Darum gings nicht 😉
    Mir gings um die pauschale Aussage "macht jedenfalls gar keinen Sinn", noch spezifischer um das "gar keinen", und das stimmt so nicht. Man kann sich durchaus Fälle vorstellen, bei denen das zumindest ein bisschen Sinn macht. Nämlich, wenn man Aufrufe verschachtelt. z.B. bei in Qt mit den ganzen Copy-On-Write Containern wäre das ein Problem.

    QVector<SomeClass> itemList() const {return m_items;}
    itemList().first();
    

    Würde evtl. schon zu einer Deep Copy führen. Und ähnliche Konstrukte sehe ich bei uns im Code sehr häufig. Deswegen find ich so eine "gar keinen Sinn" Aussage hier zu pauschal, man muss zumindest im Hinterkopf behalten, was es alles für Möglichkeiten gibt.



  • Der Thread ist ganz schön ausgeartet. Also ich bin hier bei dem Stand, dass ich wahrscheinlich noch die Auswertungen auf mein std::pair umschreibe und den Rest so lassen kann wie bei meinen letzten Post. Zumindest hat niemand weitere Einwände geäußert. Wenn ihr noch Tipps habt, die nicht den Rahmen des usecases sprengen, bin ich ganz Ohr. Ansonsten schonmal vielen Dank für die vielen Tipps und Hinweise. 👍🏻 👍🏻 👍🏻



  • @Mechanics sagte in TicTacToe:

    QVector<SomeClass> itemList() const {return m_items;}
    itemList().first();
    

    Verstehe das Beispiel nicht. Da ist die Methode const und nicht der Rückgabewert!?
    Edit:

    Ok, ich verstehe dein Beispiel. Also gut, wie immer in C++ ist eine absolute Aussage auch hier fehl am Platz.
    Nehme das "gar keinen Sinn" zurück!


Anmelden zum Antworten