Macie często taką sytuację, w której poświęcacie długie godziny na rozkminienie wszystkich możliwych wariantów danego zagadnienia, że kiedy już zaczynacie tworzyć kod to utykacie na mało istotnym, drugorzędnym problemie?

No więc ja tak miałem. Spędziłem pół dnia dłubiąc bez rezultatu kółko i krzyżyk. Ostatecznie wywaliłem wszystko w kosmos i zacząłem od początku, prościej, krócej i efektywniej.

Oto efekty tych prac.

Narysuj tablicę, wypełnij ją i zweryfikuj

Od czego zacząć pracę nad grą kółko i krzyżyk? Od … tablicy. A w zasadzie tablicy w tablicy. W moim przypadku zdecydowałem się na tablicę Stringów, choć na dobrą sprawę mogłyby to być i char’y i inty. U mnie są stringi. Inicjalizacja i druk:

Po kompilacji plansza wygląda tak:

No nie wygląda to zbyt dobrze, ale na obecną chwilę wystarczy.

Skorzystałem tu z opcji operacji for each, natomiast nie ma problemu, by zrobić to z poziomu dwóch zwykłych pętli for. Wychodzi tyle samo linijek kodu. Co istotne na tym etapie – metoda printboard zwraca tablicę jednocześnie setterem ustawiając ją jako nominalną w klasie. To o tyle ważne, że po każdym kolejnym zagraniu (ustawieniu kółka/krzyżyka w określonym polu tablicy) tablica musi zmienić przechowywane Stringi i przekazać ją do następnej rundy.

Gdy gra się rozpoczyna chciałbym zobaczyć pustą tablicę. W moim przypadku puste pola są oznaczone Stringiem „E” – Empty.

Krok 2 – ruch gracza. Ważny moment. Zakładamy, że mamy dwóch graczy. Gracz pierwszy używa do zaznaczania pola Stringa „X”, drugi – „O”. Pierwotnie stworzyłem jedną metodę makeMove, ale ostatecznie zdecydowałem się na stworzenie tożsamych dwóch metod dla dwóch graczy, tak by w interfejsie gry w jednej pętli zawrzeć dwa ruchy i wymusić przemienność (gracz pierwszy nie może wykonać ruchu zaraz po tym jak sam wykonał ruch). Wygląda to tak:

Nie ma tu większej logiki – po każdym ruchu zastępuję Stringa „E” na stringa reprezentującego gracza (Stringi graczy są ustawione przez final w polu klasy, ułatwiając tym samym modyfikacje).

Po kompilacji i uruchomieniu metody pierwszego gracza, dostajemy w efekcie:

Krok 3 – warunkowanie zwycięzcy to najtrudniejszy etap. Musimy zakodować logikę stojącą za zwycięstwem. Mamy trzy opcje na wygranie:
– ustawienie 3 znaków jednego gracza w pionie (kolumnie tablicy)
– ustawienie 3 znaków jednego gracza w poziomie (wierszu tablicy)
– ustawienie 3 znaków jednego gracza w poprzek

To tego problemu można podejść na wiele różnych sposobów, ale ja zdecydowałem się na bodaj najprostsze rozwiązanie. Napisałem trzy metody, które zwracają booleana gdy w rzędzie, kolumnie lub w poprzek znajdą się trzy TE SAME stringi. W efekcie wyszło trochę spaghetti code, dużo nadmiaru i wiele niepotrzebnego warunkowania. Ale przynajmniej jest czytelnie. A to – dla mnie – na tym etapie najważniejsze.

Jak pewnie zauważyliście, każda z tych metod zawiera jeszcze jedną metodę – checkBoardFields. Ta metoda przyjmuje trzy parametry określające lokalizację trzech stringów a następnie weryfikuje, czy są identyczne.

Ta metoda może się wydawać nadmiarowa – można ją wszak zapisać w postaci operacji warunkowej wewnątrz każdej z trzech wyżej wymienionych metod. Dla mnie jest ważna, bo sprawdza też jeden z warunków brzegowych – że pola tablicy nie są puste (muszą być różne od „E” – Empty).

Finalnie każdą z trzech metod sprawdzającą zwycięstwo spinam w finalnej metodzie boolean – checkForWin:

To tę metodą wyrzucam na interfejs, by zweryfikować czy zaistniał jeden z warunków zwycięstwa.

Interfejs, czyli sprawdźmy jak to działa

Wszystkie dotychczasowe metody wrzuciłem do klasy Game. Teraz stwórzmy interfejs w mainie i odpalmy:

Czego na obecną chwilę brakuje?
1) Weryfikacji poprawności wprowadzanych przez Scannera integer’ów
2) Weryfikacji miejsca wprowadzenia Stringa – jeżeli tam już jest X lub O, należy powtórzyć
3) Metody kończącej grę w przypadku remisu (zapełnienia tablicy bez zwycięstwa)
4) Wyprintowania informacji o tym, który zawodnik zwyciężył

To już wkrótce. Na razie dzielę się tym prostym wariantem. Całość kodu na moim githubie.