TicTacToe



  • Guten Morgen 🙂

    Ich habe ein kleines TicTacToe Spiel gebastelt. Hintergrund des Ganzen ist, dass man es einmal zusammen im Netzwerk auf einer LED-Matrix-Lampe spielen kann. Ich dachte mir jedoch bevor ich Netzwerkverkehr und das Protokoll für die Lampe implementiere (habe ich beides schon einmal gemacht) schreibe ich erst einmal so sauber wie möglich überhaupt ein TicTacToe. Da ich aber auch nur Anfänger bin, dachte ich ihr könnt mir vielleicht sagen, ob der Code "sauber" ist oder was ich verbessern muss. Ich hoffe es ist nicht zuviel Code und freue mich auf eure Tipps 😀 👍🏻

    edit: Da das Spiel sowieso auf der Lampe laufen wird habe ich mir nicht viele Gedanken um den Konsolen-Output gemacht

    main.cpp

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

    game.hpp

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

    game.cpp

    #include <vector>
    #include <iostream>
    #include <algorithm>
    #include "game.hpp"
    
    
    
    Game::Game(unsigned int width, unsigned int height, unsigned int toWin)
    	: width(width), height(height), toWin(toWin) {
    	fieldVec.assign((width * height), '0');
    	player = '1';
    	actPos = 0;
    	clipboard = fieldVec[actPos];
    	fieldVec[actPos] = cursor;
    }
    
    
    
    const std::vector<char> Game::getFieldVec() {
    	return fieldVec;
    }
    
    
    
    void Game::printField() {
    	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;
    	}
    }
    
    
    
    void Game::doAction(action selectedAction) {
    	switch (selectedAction) {
    
    		case action::moveLeft: {
    			if ((actPos%width != 0))
    				moveField(actPos - 1);
    			break;
    		}
    
    		case action::moveRight: {
    			if ((actPos + 1) % width != 0)
    				moveField(actPos + 1);
    			break;
    		}
    
    		case action::moveUp: {
    			if ((actPos >= width))
    				moveField(actPos - width);
    			break;
    		}
    
    		case action::moveDown: {
    			if (actPos < (width * height) - width)
    				moveField(actPos + width);
    			break;
    		}
    
    		case action::logIn: {
    			if (clipboard == '0') {
    				fieldVec[actPos] = player;
    				clipboard = player;
    				if (!isWinner())
    					changePlayer();
    				else
    					std::cout << "Congratulation! Player " << player << " won the Game!!!" << std::endl;
    			}
    			break;
    		}
    
    		default: {
    			std::cout << "Error: Selected Action out of range!";
    			break;
    		}
    
    	}
    }
    
    
    
    void Game::moveField(int newPos) {
    	fieldVec[actPos] = clipboard;
    	actPos = newPos;
    	clipboard = fieldVec[actPos];
    	fieldVec[actPos] = cursor;
    }
    
    
    
    void Game::changePlayer() {
    	if (player == '1')
    		player = '2';
    	else
    		player = '1';
    }
    
    
    
    bool Game::isWinner() {
    	
    	if (calculateVertical() || calculateHorizontal() || calculateDiagonalDown() || calculateDiagonalUp()) {
    		isGameOver = true;
    		return true;
    	}
    	else
    		return false;
    }
    
    
    
    bool Game::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 Game::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 Game::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 Game::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;
    }
    


  • Ich schreibe mal ein paar Anmerkungen dazu ohne Anspruch auf Vollständigkeit:

    • Muss die Windows.h wirklich benutzt werden? Das erscheint mir für so ein Konsolen spiel mehr als übertrieben. Als Linux Nutzer fühle ich mich da benachteiligt. Ich denke es ist auch nicht wirklich sinnvoll das ganze mit Pfeiltasten zu realisieren ... Nummer in Konsole eingeben (1, 2 für Spalte 1 und Zeile 2) tuts auch. Später kannst du das dann gegen eine Maus austauschen ... ob du dafür dann die WinApi nutzen möchtest oder ein anderes platformübergreifendes Framework kannst du dir dann noch überlegen.

    • Dein Klassen name finde ich nicht sehr sinvoll. Ich würde es mal Spielfeld nennen. Ob du darüber hinaus dann eine Game Klasse (die ich dann aber auch eher TicTacToe nennen würde) vlt. noch hinzufügst, könnte man sich überlegen.

    • Erstmal etwas verwunderlich fand ich, dass man scheinbar die breite und höhe deines TicTacToe Feldes festlegen kann. Ich nehme mal an, du möchtest wohl auch eine 4x4 Variante hinzufügen können etc. Das wäre ein passender use-case für non-type templates. Das hat den Vorteil, dass du auf den Vector verzichten kannst und stattdessen das std::array nehmen kannst. Das ist besser geignet, da sich die Spielfeldgröße im Laufe des Spiels ja nicht ändert und daher sollte ein statisches Array und nicht ein dynamisches Array genommen werden.

    template<unsigned int columns, unsigned int rows>
    class Field {
    
    private: 
      std::array<std::array<char, columns>, rows> field;
    }
    

    für das doppelte std::array kannst du auch einen neuen Namen einführen mittels using.

    Die drei Attribute kannst du weglassen, wenn du das Template nutzt.

    unsigned int width;
    unsigned int height;
    unsigned int toWin;
    

    Auf folgende Methode würde ich verzichten, da sie nicht gebraucht wird (eig sollte keiner Zugriff auf das Spielfeld benötigen ... jedenfalls nicht direkt). 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();
    
    • Deine Methode printField kannst du als const kennzeichnen, da sie keine Daten verändert.
      Die Implementierung kann man ein bisschen optimieren. Hier hängt es davon ab, ob du ein array der Größe width*height nimmst oder ein Array an Arrays wie ich es oben gemacht habe.
      Im ersten Fall
    for(unsigned int i = 0; i < width*height; ++i) {
      std::cout << this->fieldVec[i] << " ";
    }
    

    Wie du siehst spart man sich die extra Variable index, den genau das ist ja die Funktion von i in einer for schleife.
    Noch schöner finde ich persönlich aber C++11 range based for loops

    for(char feld : fieldVec) {
       std::cout << feld << " ";
    }
    

    bzw. mit auto

    for(auto feld : fieldVec) ...
    

    in meiner Variante dann das ganze mit doppelter for schleife. Entweder "normal" dann eben mit fieldVec[i][j] oder mit

    for(const auto& zeile : feldArray) {
      for(auto feld: zeile) {
          std::cout << feld << " ";
      }
    }
    
    • Deine isGameOver sollte eher eine Function sein, während gameOver ein privates Attribut ist. Mir stellt sich allerdings die Frage, ob man ein Attribut überhaupt benötigt oder ob die isGameOver() Methode nicht direkt ermitteln sollte, ob ein Gewinn vorliegt.
      Auch über deine private Methoden zur Gewinnermittelung würde ich mir nochmal Gedanken machen, ob das nicht effizienter geht. Man könnte z.B. drei Funktionen machen, die prüfen, ob eine zeile/spalte/diagonale den selben Wert hat.
      Für die genaue Realisierung gibt es mehrere Wege. Eine einfache Möglichkeit wäre z.B. eine Methoden prüfeGewinn(char spielerZeichen). Und dann guckt er nach, ob er dieses Zeichen in einer der Kombinationen findet. Noch schneller ist es, wenn man den letzen gesetzten Stein kennt (daher direkt im Anschluss an das setzen des Steines). Da muss man nämlich höchstens 1 Spalte, 1 Reihe und 1 Diagonale prüfen. Der Rest hat sich ja seitdem nicht geändert.

    • Statt deiner doAction() Methode würde ich eine Methode setzeStein(char spielerZeichen) machen. Das bewegen mit der Tastatur fällt ja weg (und sollte darüber hinaus auch nicht in deine Tic Tac Toe Klasse). Auch die Logik, welcher Spieler dran ist , gehört hier nicht rein. Dafür bietet sich eine seperate TicTacToe Klasse an. Was dagegen hier rein gehört, aber fehlt (zumindest sehe ich es nicht) ist eine Überprüfung, ob das Feld bereits gesetzt ist. Hier kannst du das natürlich durch ne if prüfen ... schöner wäre aber vermutlich die Einführung einer neuen Klasse (einzelnes) Feld. Das benennen der Namen überlasse ich dir, das ist hier irgendwie schwierig. Da könnte man dann eine Methode schreiben istBesetzt().
      Auch würde ich das Gewinn ermitteln seperat davon machen oder wahlweise einen bool zurückgeben. Das Ausgeben des Gewinners ist nicht die Aufgabe deines Spielfeldes. Generell nach Möglichkeit das Ui von der Logik trennen.

    Das solls erstmal gewesen sein. Insgesamt solltest du vor allem etwas an der Aufteilung der Klassen noch arbeiten ... das macht alles sehr viel lesbarer. Und nochmal die Logik etwas effizienter machen. Wenn du es nochmal überarbeitet hast, können wir ja nochmal gucken, ob man noch was verbessern kann 🙂



  • Jetzt muss ich doch was schreiben 😅

    Ich würde bei einem 1d Vector (oder Array) bleiben und eine Funktion schreiben, die die (x,y) Position auf den Index abbildet. Da sowas immer wieder vorkommt, gibt es hier im Forum sogar irgendwo ein Thread, der sich mit 2D Matritzen beschäftigt und mögliche Implementierungen aufzeigt (Edit: Gefunden: https://www.c-plusplus.net/forum/topic/348029/array2d-evolution-von-manueller-speicherverwaltung-zur-stl)

    Noch was am Rande: In der if Abfrage in deiner main() wiederholst du im Prinzip immer die gleichen 5 Zeilen Code, die würde ich an deiner Stelle in eine extre Funktion auslagern.



  • @Leon0402 sagte in TicTacToe:

    Erstmal etwas verwunderlich fand ich, dass man scheinbar die breite und höhe deines TicTacToe Feldes festlegen kann. Ich nehme mal an, du möchtest wohl auch eine 4x4 Variante hinzufügen können etc. Das wäre ein passender use-case für non-type templates. Das hat den Vorteil, dass du auf den Vector verzichten kannst

    Naja, ich sehe eher keinen Vorteil in "auf vector verzichten". Deine Variante hat sogar den Nachteil, dass man nicht als erstes im Programm nach der Spielfeldgröße fragen kann, weil man sie ja fest einkompiliert hat. Ich würde also bei vector bleiben (aber mit Schlangenmenschs Kommentar, auch wenn das bei 3x3 Tic Tac Toe eigentlich auch egal ist, Performanceprobleme sind bei dem Spiel eher nicht zu erwarten 😉 )

    Bei den restlichen Punkten stimmt ich dir zu.



  • Vielen Dank für euer Feedback. Ist ja garnicht mal so negativ ausgefallen, was mich freut 🙂
    Da ich gerade auf Arbeit bin kann ich gerade nichts editieren am Code, aber Stellung nehmen für die angesprochenen Punkte.

    Ich fange mal mit @Leon0402 an.
    Die Funktionen von der Windows.h habe ich nicht benutzt um die Linux-User zu ärgern XD. Ich benutzte selbst auch nebenbei Debian. Ich habe die Windows.h nur verwendet, weil sowieso alle Konsolenausgaben wieder rausflliegen. Das war nur gedacht, damit ich überhaupt erstmal was sehe. Wie schon gesagt, wird das Ganze mal auf einer LED Matrix stattfinden.
    Und das führt direkt zum nächsten Punkt, der variablen Spielfeldgröße. Bei standardmäßig 3x3 hätte ich die Gewinnabfragen noch mit IFs gemacht. Allerdings ist die Lampe 8x12. Da hab ich mir gedacht wäre es sinnvoll eher Funktionen zu schreiben, welche das ganze selbstständig berechnen. Das hat mir zwar viel Kopfschmerzen bereitet (auch wenn die Funktionen eigentlich einfach sind), aber es hat Spaß gemacht sich dem Anspruch zu stellen. Und genau diese Berechnungen des Gewinners haben mich auch dazu gebracht von 2D-Array zurück zu gehen (hatte ich nämlich vorher^^). Ich konnte es mir im Kopf mit einem 2D-Array einfach nicht zusammenreimem wie ich die Funktionen schreibe.
    Mit dem Klassenname hast völlig recht. "Game" war da nicht der beste Einfall XD.
    Zu den Templates. Leider hab ich ich nicht mit Templates gearbeitet (kommt aber bald in meinen C++ online Kurs und dann auch nochmal in meinem Buch) .
    Ich werde mich also als nächstes dran setzten die Klasse besser zu strukturieren wie du geschrieben hast.

    Zu @Schlangenmensch ich glaube da müsste ich komplett von vorn anfangen. Sehe ich das richtig? Bzw muss ich zumindest die Gewinnabfragen neu schreiben. Das mit der If-Abfrage kann ich natürlich problemlos ändern.



  • @wob Ich habe mir schon fast gedacht, dass du den Punkt kritisieren wirst. Und ich war schon gespannt auf die Erklärung. Ein sehr guter Einwand 😉
    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?
    Aber grundsätzlich hast du natürlich recht flexlibler ist man mit einem vector. Irgendwie finde ich es nur seltsam diesen zu benutzen, wenn die Größe eig statisch ist. Gibt aber scheinbar keine andere Möglichkeit.

    @Schlangenmensch Die Auswahl zwischen 1d und 2d hab ich mal offen gelassen. Für mich erscheint ein 2D Array logischer und einfacher zu lesen. Aber hier kommt es natürlich darauf an, ob einem Schnelligkeit vlt. wichtiger ist oder ob man 1d lesbarer findet.

    @Zhavok
    Ich verstehe. Naja muss man dann nochmal in Ruhe gucken, ob man das vlt. trotzdem noch besser hinbekommt. Es ist auf jedenfall nicht sonderlich gut lesbar. Liegt meiner Meinung auch daran, dass der vektor von vorne bis hinten durchlaufen wird ... ich hätte das eher in zwei for loops gemacht und dann zeilenweise bzw. reihenweise durchgegangen ... dann spart man sich auch das if(i % width == 0) ... man könnte ein if else aus dem anderen machen (kann man auch so durch ein bisschen Umstruktierung). Aber das größte Problem ist erstmal die Klassenstrukturierung. Um den Rest kümmern wir uns dann.
    Zu Window.h ... ich glaub dir natürlich nicht, dass du das nicht benutzt hast um Linux-User zu ägern ;P Mir ging es auch mehr darum, dass es dich von deinem Vorhaben ablenkt. Das hat auch mit dazu geführt, dass die Klassen jetzt Aufgaben übernehmen, die sie nicht übernehmen sollten. Wäre leichter, wenn man es erstmal weg lässt ... was sehen tust du auch in deiner cmd 😉



  • @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
    // ...
    

Log in to reply