C ++ OOP Tic Tac Toe
Bu, buradaki sorumun devamı niteliğindedir . Pekala, bu tam olarak bir takip değil, ama daha çok son projeden sonraki sonraki projem gibi
Nesne yönelimli programlama kullanarak bir tic tac toe oyunu yarattım
Hepiniz tic tac toe'un nasıl çalıştığını zaten biliyorsunuz, bu yüzden size nasıl çalıştığını açıklayarak zamanınızı boşa harcamayacağım.
Beni daha iyi bir programcı, özellikle daha iyi bir C ++ programcısı yapabilecek her şey, ayrıca sınıfın nasıl kullanılacağı, daha iyi işlev görmesi ve OOP'nin nasıl doğru bir şekilde kullanılacağı hakkında geri bildirim istiyorum ve bunlar:
- Optimizasyon
- Kötü uygulama ve iyi uygulama
- Kod yapısı
- Fonksiyonlar ve değişken adlandırma
- Hatalar
- Sınıf ve işlev kullanımını iyileştirme
- OOP nasıl doğru kullanılır
- Oh, ayrıca nasıl düzgün yorum eklenir
- vb
Çok teşekkür ederim!
Visual Studio Community 2019 sürüm 16.7.7 kullanıyorum
Globals.h
#ifndef GUARD_GLOBALS_H
#define GUARD_GLOBALS_H
namespace
{
enum class Players : char
{
PLAYER_X = 'X',
PLAYER_O = 'O'
};
}
#endif // !GUARD_GLOBALS_H
board.h
#ifndef GUARD_BOARD_H
#define GUARD_BOARD_H
#include "player.h"
class Board
{
private:
char board[9];
// This is suppose to be a place to put the score
// But I don't know how to implement it yet
int scoreX{};
int scoreO{};
public:
Board();
void printBoard() const;
void markBoard(const size_t& choseNum, const char& player, bool& inputPass);
char checkWin(bool& isDone, int& countTurn);
void printWinner(bool& isDone, int& countTurn);
};
#endif // !GUARD_BOARD_H
board.cpp
#include "board.h"
#include <iostream>
// To set the board with numbers
Board::Board()
{
int j{ 1 };
for (int i = 0; i < 9; i++)
{
board[i] = '0' + j++;
}
}
void Board::printBoard() const
{
system("cls");
std::cout << " | | " << "\n";
std::cout << " " << board[0] << " | " << board[1] << " | " << board[2] << "\tPlayer X: " << scoreX << "\n";
std::cout << "___|___|__" << "\tPlayer O: " << scoreO << "\n";
std::cout << " | | " << "\n";
std::cout << " " << board[3] << " | " << board[4] << " | " << board[5] << "\n";
std::cout << "___|___|__" << "\n";
std::cout << " | | " << "\n";
std::cout << " " << board[6] << " | " << board[7] << " | " << board[8] << "\n";
std::cout << " | | " << "\n\n";
}
// To change the board to which the player choose the number
void Board::markBoard(const size_t& choseNum, const char& player, bool& inputPass)
{
char checkNum = board[choseNum - 1];
// To check if the number that the player choose is available or not
if (checkNum != (char)Players::PLAYER_X && checkNum != (char)Players::PLAYER_O)
{
// To check if the number that the player input
if (choseNum >= 1 && choseNum <= 9)
{
board[choseNum - 1] = player;
inputPass = true;
}
else
{
std::cout << "CHOOSE THE AVAILABLE NUMBER!\nTRY AGAIN: ";
}
}
else
{
std::cout << "SPACE HAS ALREADY BEEN OCCUPIED\nTry again: ";
}
}
/*
There is probably a better way to do this. But, I don't know how tho
Maybe someday I could improve the checking for win but right now
this is good enough
Also, there are a lot of magic number here such as 8, 2, 6 and 7.
I've tried to remove the magic number but I don't know how.
*/
// Check the board if there is player with parallel set or not
char Board::checkWin(bool &isDone, int &countTurn)
{
/*
I use middleboard and initialize it to board[4] because in order
for a player to win diagonally they have to acquire the
middle board first. So, I initialize middleboard to board[4]
hoping it could remove the magic number
and I initialize i to 0 and j to 8 because the checking is
begin from the top left corner-middle-bottom right corner
if it false then I add add 2 to i and substract 2 from j
because now the checking is top right corner-middle-bottom left corner
*/
// Check diagonal win
size_t middleBoard = board[4];
for (size_t i = 0, j = 8; i <= 2 && j >= 6; i+=2, j-=2)
{
// If all the board is occupied by the same player then the same player win
if (middleBoard == board[i] && board[i] == board[j])
{
//This is suppose to add score, but I don't know how to implement it yet
board[middleBoard] == (char)Players::PLAYER_X ? scoreX++ : scoreO++;
isDone = true;
return middleBoard; // To return the character of the player who won
}
}
/*
I initialize initialNum to 0 as a starting point for the checking.
Initilialized i to 1 and j to 2
The checking is like this, top left corner-middle top-top right corner
If it false then the I add 3 to initialNum to make middle left as the
starting point, then add 3 to i and j so it the next checking is
middle left-middle-middle right, and so on
*/
// Check horizontal win
size_t initialNum = 0;
for (size_t i = 1, j = 2; i <= 7 && j <= 8; i += 3, j += 3)
{
if (board[initialNum] == board[i] && board[i] == board[j])
{
board[initialNum] == (char)Players::PLAYER_X ? scoreX++ : scoreO++;
isDone = true;
return board[initialNum];
}
else
{
initialNum += 3;
}
}
/*
I reset the initialNum to 0 and initialized i to 3 and j 6 so
the first check will be like this: top left corner-middle left-bottom left corner
if it fails then i add 1 to initialNum, i, and j, so the next check will be
middle top-middle-middle bottom and so on
*/
// Check vertical win
initialNum = 0;
for (size_t i = 3, j = 6; i <= 5 && j <= 8; i++, j++)
{
if (board[initialNum] == board[i] && board[i] == board[j])
{
board[initialNum] == (char)Players::PLAYER_X ? scoreX++ : scoreO++;
isDone = true;
return board[initialNum];
}
else
{
initialNum++;
}
}
// If the countTurn is 8 then there're no place to occupy anymore, thus a draw
if (countTurn == 8)
{
isDone = true;
return 'D'; // As a check for printWinner() function
}
countTurn++;
}
// To print who's the winner or draw
void Board::printWinner(bool& isDone, int& countTurn)
{
if (checkWin(isDone, countTurn) == 'D')
{
std::cout << "It's a Draw!\n";
}
else
{
std::cout << "Congratulations!\nPlayer " << checkWin(isDone, countTurn) << " won the game!\n";
}
}
player.h
#ifndef GUARD_PLAYER_H
#define GUARD_PLAYER_H
#include "Globals.h"
#include "board.h"
class Board;
class Player
{
private:
char mainPlayer;
char secondPlayer;
char turnPlayer = mainPlayer;
public:
void choosePlayer(bool &choosePass);
void movePlayer(Board& myBoard);
void switchPlayer();
};
#endif // !GUARD_PLAYER_H
player.cpp
#include "player.h"
#include "board.h"
#include <iostream>
#include <random>
// To give a choice for the player if they want to be X or O
void Player::choosePlayer(bool& choosePass)
{
char chosePlayer;
std::cout << "Do you want to be player X or O? ";
while (!choosePass)
{
std::cin >> chosePlayer;
// If the player type X uppercase or lowercase then they will be
// X and the computer will be O, vice versa
if (chosePlayer == 'x' || chosePlayer == 'X')
{
mainPlayer = (char)Players::PLAYER_X;
secondPlayer = (char)Players::PLAYER_O;
choosePass = true;
}
else if (chosePlayer == 'o' || chosePlayer == 'O')
{
mainPlayer = (char)Players::PLAYER_O;
secondPlayer = (char)Players::PLAYER_X;
choosePass = true;
}
else
{
std::cout << "Invalid choice\n Try again: ";
}
}
}
// To make a player choose a number to which they want to occupy
void Player::movePlayer(Board &myBoard)
{
size_t choseNum;
bool inputPass = false;
/*
I make it turnPlayer != mainPlayer because if I make it
turnPlayer == mainPlayer then the computer will make the first move
I don't know why. Probably should find out the why. But it'll do for now
*/
// If turnPlayer is not mainPlayer then it's the player's move
if (turnPlayer != mainPlayer)
{
std::cout << "Player " << mainPlayer << " choose a number: ";
while (!inputPass)
{
if (std::cin >> choseNum)
{
myBoard.markBoard(choseNum, mainPlayer, inputPass); //Go to markBoard function in board.cpp
}
else
{
std::cout << "Invalid input type (Type only number)\nTry again: ";
std::cin.clear(); // To clear the input so
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // the player can input again
}
}
}
// If the turnPlayer is mainPlayer then it's the computer's move
else
{
while (!inputPass)
{
// To make a random move for the computer
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> distrib(1, 9);
choseNum = distrib(gen);
myBoard.markBoard(choseNum, secondPlayer, inputPass);
}
}
}
// To change turn, if the player finishes then the computer will make the move
void Player::switchPlayer()
{
turnPlayer = (turnPlayer == mainPlayer) ? secondPlayer : mainPlayer;
}
main.cpp
#include "board.h"
#include "player.h"
int main()
{
Board myBoard;
Player mainPlayer;
int countTurn{ 0 };
bool choosePass = false;
bool isDone = false;
myBoard.printBoard(); // To print the initial board with numbered spaces
while (!isDone)
{
if (!choosePass)
{
mainPlayer.choosePlayer(choosePass);
}
mainPlayer.movePlayer(myBoard);
myBoard.printBoard();
mainPlayer.switchPlayer();
myBoard.checkWin(isDone, countTurn);
}
myBoard.printWinner(isDone, countTurn);
}
Yanıtlar
Olmalı mı Globals.h?
Katılmıyorum. Globals.hBir single vardır enumsizin için tek anlamlıdır Playersınıfın. Öyleyse neden yeni bir Başlık oluşturmalı? Neden olamaz enum class Playerssadece olmak Player.cpp? Bu, içeriğine erişen tek dosyadır Players. Burada yapılacak en iyi şeyin anonim bir ad alanıPlayer.cpp oluşturmak ve orada kalmasına izin vermek olduğuna inanıyorum .
// Player.cpp
namespace {
enum class Players { ... };
}
Ayrıca, başlık dosyasında adsız bir ad alanı kullanırken dikkatli olun
Std :: tolower kullanın
Bir karakterin her iki durumu ile karşılaştırmak yerine, bir karakteri std::tolowerdoğrudan küçük harfe dönüştürmek için kullanın . Bu dönüşürdü
std::cin >> chosePlayer;
if (chosePlayer == 'x' || chosePlayer == 'X') {...}
else if (chosePlayer == 'o' || chosePlayer == 'O') {...}
else {...}
İçine
std::cin >> chosePlayer;
chosePlayer = std::tolower(chosePlayer, std::locale());
if (chosePlayer == 'x' ) {...}
else if (chosePlayer == 'o') {...}
else {...}
#include <locale>
- 1'den fazla karakter girildiğinde, kodun ilkini kabul edeceğini unutmayın. Örneğin, kullanıcı girerse
cplusplus,chosePlayerşimdi olarak ayarlanmıştırc.
enum classYarattığınızı kullanın
enumBüyüyü kaldırarak yarattınız xve o. Neden hala burada kullanıyorsun?
if (chosePlayer == 'x' )
else if (chosePlayer == 'o')
enum class PlayersBuradaki değerleri kullanın .
enumBurada bir kullanın
Bazıları aynı fikirde olmasa da, bence buraya göre enumdaha iyi enum class. Bunun nedeni, charbir enumve chartürünü karşılaştırmak istediğinizde değerleri sürekli olarak dönüştürmek zorunda olmamanızdır .
Daha .cppönce bahsettiğim gibi yalnızca tek bir dosyada görünecekse, büyük olasılıkla ad çatışmaları yaşamazsınız.
enum Player : char { PLAYER_1 = 'x', PLAYER_2 = 'o' };
Nereden Player::chosePlayer()
void Player::choosePlayer(bool& choosePass)
{
char chosePlayer;
std::cout << "Do you want to be player X or O? ";
while (!choosePass)
{
std::cin >> chosePlayer;
// If the player type X uppercase or lowercase then they will be
// X and the computer will be O, vice versa
if (chosePlayer == 'x' || chosePlayer == 'X')
{
mainPlayer = (char)Players::PLAYER_X;
secondPlayer = (char)Players::PLAYER_O;
choosePass = true;
}
else if (chosePlayer == 'o' || chosePlayer == 'O')
{
mainPlayer = (char)Players::PLAYER_O;
secondPlayer = (char)Players::PLAYER_X;
choosePass = true;
}
else
{
std::cout << "Invalid choice\n Try again: ";
}
}
}
Girilen değerlerin iyi mi yoksa kötü mü olduğunu belirtmek istiyorsanız, neden bir booldeğişkene bir referans iletiyorsunuz? trueGiriş iyiyse ve giriş iyi değilse neden geri dönmeyesiniz false? Başvuruya göre geçiş örtük olarak bir işaretçi iletir, bu nedenle aslında işlevdeki bir bool değişkenine bir işaretçi geçirirsiniz. Sen edecektir var Eğer mevcut mantıkla giderseniz referans olarak geçmesine ama şeydir
sizeof(bool) == 2
sizeof(bool*) == 8
Bu nedenle ve basitleştirmek için, geri dönmenin Trueveya Falsedaha iyi olacağına inanıyorum.
Kazananı kontrol etmek
Bir kazananı kontrol etmek için mevcut algoritmanız çok uzun ve okunması zor. Daha iyi yollar var. Bu iş parçacığı onlar hakkında birçok yararlı bilgi sağlayacaktır . Hepsinden en basiti
constexpr int NB_WIN_DIR = 8;
constexpr int N = 3; // please think of a better name
constexpr int wins[NB_WIN_DIR][N] {
{0, 1, 2}, // first row
{3, 4, 5}, // second row
{6, 7, 8}, // third row
{0, 3, 6}, // first col
{1, 4, 7}, // second col
{2, 5, 8}, // third col
{2, 4, 6}, // diagonal
{0, 4, 8}, // antidiagonal
};
for (int i = 0; i < NB_WIN_DIR ;i++)
{
if (board[wins[0]] == board[wins[1]] and board[wins[1]] == board[wins[2]])
return board[wins[0]];
}
Ne zaman geçmelisin const&?
A const bool&ve const size_t&function argümanları görüyorum .
Ne zaman gereken sabit bir referans olarak geçmektedir
- Büyük nesnelerin kopyalarından kaçınmak istediğinizde
Daha önce de söylediğim gibi, başvuruya göre geçiş örtük olarak bir işaretçi geçer. Ama sorun şu ki
sizeof(bool) == 2
sizeof(bool*) == 8
sizeof(size_t) == 8 // depending on your machine, sometimes 4
sizeof(size_t*) == 8
Yani en iyi ihtimalle, size hiç iyi gelmiyor ve muhtemelen daha çok kötü yapıyor . Basit bir kural, int, char, double, floatby gibi ilkel türleri geçmek zorunda değilsiniz const&, ancak, eğer benzer bir şeyiniz varsa, referansla geçin std::vector.
Beni yanlış anlamayın, size gereken bir işlev, bir nesnenin orijinal değerini değiştirerek gerekip gerekmediğini referans olarak geçmektedir. Ancak amaç bu değilse, yalnızca büyük nesneler için kullanın.
Kod yapınızı yeniden düşünün
Bu dersi gerçekten sevmiyorum
class Player
{
private:
char mainPlayer;
char secondPlayer;
char turnPlayer = mainPlayer;
public:
void choosePlayer(bool &choosePass);
void movePlayer(Board& myBoard);
void switchPlayer();
};
Sizin Playersınıf tek bir oyuncu hakkında herhangi bir bilgi tutmaz. Tüm üye işlevleriniz board. Bunların hepsi aslında sizin Boardsınıfınıza ait. Bir oyuncu aslında sadece bir char, ya oda x. Kelimenin tam anlamıyla bundan başka hiçbir bilgi tutmaz. Yapmanız gereken, zaten yaptığınız gibi bir sıralama kullanan bir oyuncuyu temsil etmektir.
enum Player { ... };
class Board{
Player human;
Player bot;
};
botsize karşı oynuyor bilgisayar olacaktır ve humangerçek kullanıcı olurdu.
Bir sınıf kullanılarak temsil edilmesi gereken düşünce basit bir harekettir. Bir hareketin iki şeyi vardır.
- Kare
- Oyuncu
Programınızın her yerinde, bu ikisini ayrı ayrı geçtiniz, neden structonu tutacak bir basit yaratmıyorsunuz?
struct Move {
int square;
Player player;
}
Bu oyunun nasıl yeniden yapılandırılabileceğine dair çok temel bir örnek yazdım.
class Game
{
private:
struct Move {
Player player;
int square;
Move(const int square, const Player player)
: square(square), player(player)
{}
};
enum Player {
PLAYER_1, PLAYER_2, NONE
};
template < typename T, size_t N > using array = std::array < T, N >;
array < char, NB_SQ > board;
Player human;
Player bot;
short int turns; // number of total moves played
void computer_move();
Move input_move() const;
void make_move(const Move& move);
bool validate_move(const Move& move);
Player check_win() const;
bool check_draw() const;
void print_board() const;
void new_game(); // choose whether the player plays 'x' or 'o' here
public:
void mainloop(){
for (;;) {
const Move& move = input_move();
make_move(move);
computer_move();
if (check_win()) // ...
if (check_draw()) // ...
}
}
Game() { new_game(); }
};
int main() {
Game game;
game.mainloop();
}
hakkında system("cls")
Mevcut programınız, Windows olmayan işletim sistemlerinde çalışmayacaktır. Diğer sistemlerin çoğunda, kelime clear. Bunu daha taşınabilir hale getirmek için işletim sistemini kontrol etmek üzere bir #ifdef ifadesi kullanabilirsiniz.
void clear_screen()
{
#ifdef _WIN32
system("cls");
#else
system("clear");
#endif
}
Devamını oku
Genel Gözlemler
Kod main()iyi boyutlandırılmış, hoş ve sıkı, okunabilir. Tek main()olumsuz yanı, gerçekten gerekli olmayan yorumdur.
Board ve Player arasında karşılıklı bağımlılıklar var gibi görünüyor, yazılım tasarımında bu sıkı bir bağlantı olarak bilinir ve genellikle kötü bir tasarımı gösterir.
Player sınıfının yalnızca bir örneğini görüyorum ve her oyuncu için bir tane olmak üzere 2 örnek görmeyi bekliyorum.
Sıkı kuplajı kaldırmak için nesne tasarımlarınız üzerinde çalışmaya devam edin ve SOLID programlama ilkelerini takip etmeye çalışın . Kompozisyon gibi bazı nesneye yönelik tasarım modellerini öğrenin.
SOLID, yazılım tasarımlarını daha anlaşılır, esnek ve sürdürülebilir hale getirmeyi amaçlayan beş tasarım ilkesinin kısaltmasıdır. Bu, nesnelerinizi ve sınıflarınızı daha iyi tasarlamanıza yardımcı olacaktır.
- Tek Sorumluluk Prensibi - Bir sınıf yalnızca tek sınıfın şartname etkilemesi mümkün olmalıdır yazılımın şartnamenin bir parçası olarak değişir, tek bir sorumluluğa sahip olmalıdır.
- Açık kapalı İlke - uzantısı için açık olmalıdır yazılım varlıkları (vb sınıflar, modüller, fonksiyonlar,) devletler, ancak değişiklik için kapandı.
- Liskov değiştirme İlke - Bir programda nesneler o programın doğruluğunu değiştirmeden kendi alt türleri örnekleri ile değiştirilebilir olmalıdır.
- Arayüz ayrımı ilkesi - hayır istemci kullanmaz yöntemlere bağlıdır zorunda gerektiğini belirtir.
- Bağımlılık Inversion Prensibi - yazılım modüllerini ayırma belirli bir formudur. Bu prensibi takip ederken, yüksek seviyeli, politika belirleyen modüllerden düşük seviyeli bağımlılık modüllerine kadar kurulan geleneksel bağımlılık ilişkileri tersine çevrilir, böylece yüksek seviyeli modüller düşük seviyeli modül uygulama detaylarından bağımsız hale gelir.
Yüksek Düzeyde Bir Uyarı Açın, Uyarıları Yoksaymayın
Derlediğimde 2 uyarı var ve her iki uyarı da kodda olası mantık problemlerini gösteriyor.
Bir uyarı, bu satırdaki olası veri kaybıdır:
return middleBoard; // To return the character of the player who won
içinde Board::checkwin(). Bu uyarı, kodun . size_tOlarak bildirilmiş bir değişkeni döndürmesidir char.
İkinci uyarı da , fonksiyonun son satırında verilen Board::checkwin()uyarı ile not all control paths return a valueilgilidir. Bu, koddaki olası mantık sorunlarını kesinlikle gösterdiği için 2 uyarıdan daha ciddi olabilir.
Eski C Stili Dökümler Yerine C ++ Stili Yayınları Tercih Etme
Aşağıdaki kod satırı eski bir C stili döküm kullanıyor:
board[initialNum] == (char)Players::PLAYER_X ? scoreX++ : scoreO++;
C ++, daha iyi uyarılar ve derleyici hataları sağlayan kendi yayınlarına sahiptir, bunlar static castsve dynamic casts. Statik yayınlar derleme zamanında gerçekleşir ve dönüştürme türü güvenli değilse olası hataları veya uyarıları sağlar. Kod satırında statik bir atama daha uygundur.
board[initialNum] == (static_cast<char>(Players::PLAYER_X)) ? scoreX++ : scoreO++;
Yorumların Yerine Kendini Belgeleyen Kodu Tercih Et
Kodda çok fazla yorum var. Yeni programcıların farkında olmadıkları şeylerden biri kodun bakımıdır, yazdığınız kod 20 yıl veya daha uzun süredir kullanılıyor olabilir ve şirket için o kadar uzun süre çalışmamanız oldukça olasıdır. Kodda çok sayıda yorum varsa, yorumların yanı sıra kodun kendisi de korunmalıdır ve bu yapılacak iş miktarını iki katına çıkarabilir. Açık değişken, sınıf ve işlev adları kullanarak kendi kendini belgeleyen kod yazmak daha iyidir. Tasarım kararları veya üst düzey soyutlamalar için yorumları kullanın. Bir işlev özel bir akış durumu gerektiriyorsa, işlevden önceki bir açıklama bloğunda bulunur.
KURU Kodu
Bazen KURU kod olarak da anılan Kendini Tekrar Etme İlkesi adlı bir programlama ilkesi vardır . Kendinizi aynı kodu birden çok kez tekrarlarken bulursanız, onu bir işlevde kapsüllemek daha iyidir. Yinelemeyi de azaltabilecek kodda döngü yapmak mümkünse. İşlev Board::checkWin(), kazançları kontrol eden 3 döngüde fazladan kod içerir. Bunu düzeltmenin birçok yolu vardır ve başka bir cevapta iyi bir yol önerilmiştir.
Karmaşıklık
İşlev Board::checkWin()çok karmaşık (çok fazla yapıyor). Bir karakter döndürmek yerine Board::checkWin(), bir kazanç olup olmadığını gösteren bir boole değeri döndürmelidir. Diğer işlevler, panoyu uygun karakterlerle güncellemeyi uygulamalıdır. Bu işlevin karmaşıklığı uyarıya yol açmıştır not all control paths return a value.
Sihirli Sayılar
Board::checkWin()İşlevin her bir döngüsünde kazanç olup olmadığını kontrol eden Sihirli Sayılar vardır, kodu daha okunaklı ve bakımı daha kolay hale getirmek için onlar için sembolik sabitler oluşturmak daha iyi olabilir. Bu numaralar pek çok yerde kullanılabilir ve sadece bir satırı düzenleyerek değiştirebilmek bakımı kolaylaştırır.
Koddaki sayısal sabitlere bazen Sihirli Sayılar denir , çünkü onlar için açık bir anlam yoktur. Stackoverflow'da bununla ilgili bir tartışma var .