Kółko i krzyżyk - opinie, uwagi

0

Witam,

Dzisiaj udało mi się napisać grę znaną popularnie jako ‘Kółko i krzyżyk’. Chciałbym się nią z Wami podzielić i byłbym bardzo wdzięczny, gdybyście podzielili się swoją opinią na temat tej napisanej przeze mnie gry. Mam tu na myśli oczywiście Waszą opinię, Wasze uwagi pod kątem napisanego przeze mnie kodu.

Moim priorytetowym założeniem podczas pisania tej aplikacji było to, aby napisać ją w pełni obiektowo. Mówię to, ponieważ do tej pory miałem problemy, żeby przestawić się na pisanie obiektowe programów, sprawiało mi to trudność.

Także w załączniku przesyłam Wam plik .exe wraz z kodem.

Liczę na Wasze opinie, uwagi oraz mile widziana jest konstruktywna krytyka :)

0

Naprawdę nikt nie ma żadnej opinii i uwag co do tego kodu? :/

Wprowadziłem dodatkowo tryb gry z komputerem. Także w załączniku przesyłam nowszą wersję tej gry. Cały czas staram się ją dopracowywać i dodawać nowe elementy do rozgrywki. Planuję wprowadzić też małe AI dla komputera, który blokował by nas przed podstawieniem trzeciego znaku z pionie, poziomie, bądź ukośnie.

Jednak bardziej zależy mi na razie na Waszych uwagach co do obiektowości mojego kodu. Chodzi o to, czy rozumiem samą ideę korzystania i używania klas, metod oraz obiektów. Czy dziedziczenie byłoby w tej mojej aplikacji gdziekolwiek potrzebne? Jeśli tak to może jakieś wskazówki byście mieli jak optymalnie zaktualizować ten kod, aby był jeszcze bardziej postrzegany jako orientowany obiektowo.

Bardzo liczę na Wasze uwagi.

Pozdrawiam.

1

jak na moje oko, to kod wygląda w miarę ok. Trochę za dużo tych białych linii. W metodzie Map::CheckResultGame mógłbyś napisać to lepiej, gdyż ten wielki if jest nieczytelny. Możesz to rozbić na np trzy metody, które sprawdzają czy:

  1. Ktoś ma 3 ukośne
  2. Ktoś ma 3 poziome
  3. Ktoś ma 3 pionowe
    i wygrywa :) Do logiki nic to nie wniesie, ale na pewno podniesie czytelność. Mógłbym się poczepiać jeszcze tej metody, ale to by było czyste marudztwo :) Myślę, że ideę OOP czaisz, teraz tylko kodzić, kodzić i jeszcze raz kodzić
0

Postanowiłem napisać gierkę od nowa.
Byłbym bardzo wdzięczny, gdybyście mogli spojrzeć na mój nowy, poprawiony kod i sprawdzić czy jest napisany lepiej pod względem obiektowym niż poprzedni.

W załączniku przesyłam projekt.

Pozdrawiam :)

1

już dużo lepiej. Teraz możesz zrobić prostą sztuczną inteligencję i gra gotowa :)

4

@XardasLord, więc tak:

  1. Nie stosujesz się do konwencji pisania kodu w C#. Nazwy zmiennych zaczynasz wielkimi literami, nazwy metod małymi, itd.
  2. Czemu Object i String, a nie object i string?
  3. Tworzysz jakieś gettery (metody zaczynające się od get) zamiast właściwości.
  4. W klasie Game - Object x; - taka nazwa pola nic nie mówi.
  5. Czemu konstruktor klasy Game przyjmuje typ Object? Przecież tam ewidentnie powinien być Player!
  6. Obiekt typu Random powinien być statycznym polem klasy, a nie za każdym razem tworzony na nowo w metodzie. Tutaj nie ma tego problemu, ale gdybyś wywoływał ją bardzo szybko po sobie, to dostawałbyś nielosowe wyniki.
  7. To co jest w metodzie Run klasy ConsoleApplication mogłoby być w metodzie Main klasy Program. Klasa ConsoleApplication jest zupełnie niepotrzebna.
  8. Po co klasa ShowData, skoro całą resztę komunikatów i tak masz w klasie Game?
  9. Metoda Play jest okropna. Ma aż 110 linijek, przy czym znajduje się tam właściwie dwukrotnie skopiowany identyczny kod. Przede wszystkim należałoby wydzielić z niej kod odpowiedzialny za pobieranie imion użytkowników do oddzielnej metody, a gdy już to zrobisz, to powinieneś zauważyć, że główna pętla gry jest identyczna, bez względu na to, czy gra człowiek z człowiekiem, czy z komputerem.
  10. Te Twoje klasy są w ogóle ze sobą nie połączone. Moim zdaniem klasa Game powinna mieć w sobie pola typu Map, i dwa typu Player, ustawiane w konstruktorze. Wówczas byłoby widać, że coś tu się z czymś wiąże, bo na razie to jest tylko programowanie strukturalne z klasami.

Tyle ode mnie, wkleję jeszcze tylko log niefajnych rzeczy w kodzie:

Solution Kolko i krzyzyk.sln
Project Kolko i krzyzyk
Kolko i krzyzyk\ComputerPlayer.cs:2 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\ComputerPlayer.cs:3 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\ComputerPlayer.cs:4 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\ComputerPlayer.cs:23 Use implicitly typed local variable declaration
Kolko i krzyzyk\ComputerPlayer.cs:24 Local variable 'possition' hides field 'int Kolko_i_krzyzyk.Player.possition'
Kolko i krzyzyk\ConsoleApplication.cs: Local variable 'choice' can be declared in inner scope
Kolko i krzyzyk\ConsoleApplication.cs:2 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\ConsoleApplication.cs:3 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\ConsoleApplication.cs:4 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\ConsoleApplication.cs:12 Empty constructor is redundant. The compiler generates the same by default.
Kolko i krzyzyk\ConsoleApplication.cs:20 Local variable 'choiceString' can be declared in inner scope
Kolko i krzyzyk\ConsoleApplication.cs:33 Possible 'null' assignment to entity marked with 'NotNull' attribute
Kolko i krzyzyk\ConsoleApplication.cs:54 Redundant 'String.Format()' call
Kolko i krzyzyk\Game.cs:2 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Game.cs:3 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Game.cs:4 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Game.cs:10 Field can be made readonly
Kolko i krzyzyk\Game.cs:11 Field can be made readonly
Kolko i krzyzyk\Game.cs:25 Local variable 'possition' can be declared in inner scope
Kolko i krzyzyk\Game.cs:26 Join declaration and assignment
Kolko i krzyzyk\Game.cs:26 Join declaration and assignment
Kolko i krzyzyk\Game.cs:27 Use implicitly typed local variable declaration
Kolko i krzyzyk\Game.cs:36 Use implicitly typed local variable declaration
Kolko i krzyzyk\Game.cs:37 Use implicitly typed local variable declaration
Kolko i krzyzyk\Game.cs:81 Local variable 'possition' can be declared in inner scope
Kolko i krzyzyk\Game.cs:82 Join declaration and assignment
Kolko i krzyzyk\Game.cs:83 Use implicitly typed local variable declaration
Kolko i krzyzyk\Game.cs:88 Use implicitly typed local variable declaration
Kolko i krzyzyk\Game.cs:89 Use implicitly typed local variable declaration
Kolko i krzyzyk\HumanPlayer.cs:2 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\HumanPlayer.cs:3 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\HumanPlayer.cs:4 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\HumanPlayer.cs:31 Possible 'null' assignment to entity marked with 'NotNull' attribute
Kolko i krzyzyk\HumanPlayer.cs:37 Redundant 'String.Format()' call
Kolko i krzyzyk\Map.cs: Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Map.cs:2 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Map.cs:4 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Map.cs:10 Field can be made readonly
Kolko i krzyzyk\Map.cs:79 For-loop can be converted into foreach-loop
Kolko i krzyzyk\Player.cs:2 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Player.cs:3 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Player.cs:4 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Player.cs:10 Name 'possition' does not match rule 'Fields (not private)'. Suggested name is 'Possition'.
Kolko i krzyzyk\Player.cs:11 Name 'sign' does not match rule 'Fields (not private)'. Suggested name is 'Sign'.
Kolko i krzyzyk\Player.cs:12 Name 'name' does not match rule 'Fields (not private)'. Suggested name is 'Name'.
Kolko i krzyzyk\Player.cs:15 Convert constructor to protected
Kolko i krzyzyk\Player.cs:18 Convert constructor to protected
Kolko i krzyzyk\Player.cs:25 Name 'getField' does not match rule 'Methods, properties and events'. Suggested name is 'GetField'.
Kolko i krzyzyk\Player.cs:27 Name 'getSign' does not match rule 'Methods, properties and events'. Suggested name is 'GetSign'.
Kolko i krzyzyk\Player.cs:32 Name 'getName' does not match rule 'Methods, properties and events'. Suggested name is 'GetName'.
Kolko i krzyzyk\Program.cs:7 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Program.cs:8 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Program.cs:9 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\Program.cs:15 Parameter 'args' is never used
Kolko i krzyzyk\Program.cs:17 Use implicitly typed local variable declaration
Kolko i krzyzyk\Properties\AssemblyInfo.cs:2 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\ShowData.cs:2 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\ShowData.cs:3 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\ShowData.cs:4 Using directive is not required by the code and can be safely removed
Kolko i krzyzyk\ShowData.cs:10 Name 'winMessage' does not match rule 'Methods, properties and events'. Suggested name is 'WinMessage'.

0

@somekind wielkie dzięki za swoje spostrzeżenia i uwagi co do mojego kodu. Zastosowałem się do nich i kod wygląda teraz o wiele przyjaźniej. Oddzieliłem także metody używające WriteLine od logiki.
Mam pytanie. Nie mam pojęcia jak mogę zastąpić inaczej odróżnianie gry vsPlayer od vsComputer... Bo teraz ciągle służy mi do tego ten if:

if (player is HumanPlayer)

Efekt końcowy projektu wygląda następująco:

3

@XardasLord, jest pewien postęp, ale zostało jeszcze dużo do zrobienia.

Przede wszystkim - dobrze, że wyodrębniłeś klasy: Game, Map, i dwa typy Playerów. Dobrze, że zrobiłeś oddzielną klasę do wyświetlania informacji na konsoli, chociaż nazwa ShowData nie jest jakaś super. Trochę się jednak gubisz, zarówno w projekcie, jak i w języku. Ale po kolei:

  1. Zamiast Console.Write i \n na końcu tekstu używaj Console.WriteLine.

  2. Ten warunek: while (choice != 0 || choice < 1 && choice > 2) można znacznie uprościć, przecież choice < 1 && choice > 2 nie jest chyba nigdy prawdziwe, prawda?

  3. Ten Twój if z is jest dobry, nawet bardzo dobry... ale zupełnie niepotrzebny. Gra wygląda identycznie, bez względu na to kto gra. Spójrz na swoją główną pętlę gry:

// GŁÓWNA PĘTLA GRY
do
{
    if (PlayPlayer(map, player1, possition))
        break;

    if (PlayPlayer(map, player2, possition))
        break;
                    
} while (true);

Ten sam kod powielasz dwukrotnie, po co? Po to właśnie masz wspólną klasę dla obu typów graczy, żeby klasa Game nie musiała ich wewnętrznie rozróżniać. (Na marginesie - zamiast pętli do...while możesz tu użyć po prostu while.)

  1. Jestem pewien, że possition pisze się przez jedno "s". ;)

  2. Spójrzmy teraz na ten fragment kodu:

if (player is HumanPlayer)
            {
                string playername;

                playername = showdata.GetPlayer1Name();
                HumanPlayer player1 = new HumanPlayer('X', playername);
                playername = showdata.GetPlayer2Name();
                HumanPlayer player2 = new HumanPlayer('O', playername);

Masz już utworzony obiekt gracza (player). Następnie tworzysz jeszcze dwóch graczy player1 i player2. Więc masz trzech graczy, z czego gra tylko dwóch. Nie sądzisz, że coś tu jest nie tak?

  1. Cały kod klasy Game można znacznie uprościć, wystarczy w nim właściwie tyle:
namespace Kolko_i_krzyzyk
{
    class Game
    {
        private readonly ConsoleOperations consoleOperations = new ConsoleOperations();
        private readonly Map map;
        private readonly Player first;
        private readonly Player second;

        public Game(Map map, Player first, Player second)
        {
            this.map = map;
            this.first = first;
            this.second = second;
        }

        public void Play()
        {
            while(true)
            {
                if (PlayPlayer(first))
                    break;

                if (PlayPlayer(second))
                    break;
            } 
        }

        public bool PlayPlayer(Player player)
        {
            int position = player.GetField(map);
            map.PutSign(position, player.Sign);
            
            consoleOperations.DrawMap(map);

            if (map.CheckWin(player))
            {
                consoleOperations.WinMessage(player.Name);
                return true;
            }

            if (map.CheckDraw())
            {
                consoleOperations.DrawMessage();
                return true;
            }

            return false;
        }
    }
}

(Zmieniłem ShowData na ConsoleOperations, aby nazwa była bliższa rzeczywistości.)
Nie jest jeszcze doskonały, ale chodzi mi o ideę - kod może być dużo prostszy, bo istnienie różnych typów graczy nie zmienia zasad gry.

  1. Druga sprawa, po co zarówno w HumanPlayer jak i ComputerPlayer masz taki kod:
public override char Sign
{
    get 
    { 
        return this.sign; 
    }
}


public override string Name
{
    get 
    { 
         return this.name; 
    }
}

Skoro to jest identyczne, to powinno być w klasie bazowej! To powinny być zwykłe właściwości, a nie abstrakcyjne, skoro ich implementacja jest identyczna.
Abstrakcyjne ma być to, co może się różnić w klasach potomnych. W tym przypadku jest to wybór pola do następnego ruchu, czyli metoda GetField i nic więcej.

  1. W kilku miejscach programu sprawdzasz taki warunek:
    map.ArrayMap[possition] == 'X' || map.ArrayMap[possition] == 'O'
    Nie lepiej byłoby napisać metodę w klasie Map, która by sprawdzała, czy pole jest wolne? Dzięki temu niepotrzebna byłaby zupełnie właściwość ArrayMap.

  2. Masz dwie takie funkcje:

public string GetPlayer1Name()
{
    Console.Write("\n\nPodaj imię pierwszego gracza (X): ");
    string playername = Console.ReadLine();

    return playername;
}

public string GetPlayer2Name()
{
    Console.Write("\n\nPodaj imię drugiego gracza (O): ");
    string playername = Console.ReadLine();

    return playername;
}

Różnią się między sobą jednie w dwóch miejscach. Wiesz chyba, że można to uprościć.

  1. Za to pomysł wyłączania aplikacji gdy użytkownik wpisze litery zamiast cyfr bardzo mi się podoba. Od razu nauczy się tych durnych użytkowników uwagi! ;)
    A tak na serio - zrób to w sposób wygodny dla użytkownika. Użyj metody int.TryParse i sprawdzaj, czy użytkownik wpisał liczbę. Jeśli nie, to poproś go o powtórkę. Ale nigdy nie wyłączaj aplikacji po błędnym zachowaniu użytkownika!

  2. Klasa Map ma reprezentować planszę do gry. Czyli powinna móc:
    a) powiedzieć, jaki znak jest na którym polu;
    b) ustawić znak na wskazanym polu;
    c) powiedzieć, czy dane pole jest wolne;
    d) powiedzieć, ile wolnych pól zostało.

I to wszystko. Metody sprawdzające wynik gry powinny się znajdować w klasie Game, a nie Map. Nawet w rzeczywistym świecie, to nie kartka papieru zna zasady gry w kółko i krzyżyk.
Gdy przeniesiesz metody CheckWin i CheckDraw to klasy Game, kod się jeszcze skróci i uprości.

0

A jak juz bedziesz mial ladny, w pelni napisany kod obiektowy (wiem ze takie miales zalozenie i dzieki Somekindowi troche się nauczyles), przejrzyj sobie pierwsze slajdy stad: http://www.slideshare.net/Reg1773/puapki-programowania-obiektowego zobaczysz wtedy ze uzywanie na sile obiektowosci komplikuje calosc. Oraz ze nie ma uniwersalnych rozwiazan, obiektowosc, wzorce projektowe itp. sa dobre, ale musza byc stosowane z glowa.

1 użytkowników online, w tym zalogowanych: 0, gości: 1