Projekt Szachów

0

Próbuję napisać szachy, ale mam ogromny kłopot z projektem.
Szukałem na forum, ale nie zauważyłem niczego co by mi pomogło.

Czy pozycję poszczególnych bierek powinna trzymać plansza, czy same bierki?
Czy bierki powinny trzymać swoją ikonę, czy grafikę trzymać jakoś osobno i trzymać ikony(linki) w jakimś xmlu?
Możliwe ruchy powinny być określane w klasie planszy, zawodnika, czy samych bierek?
Ocena sytuacji powinna być w klasie zawodnika?

Jak Wy byście rozdzielili zadania, zrobili komunikację między klasami, i z grafiką?
Z góry dziękuję za wszelką pomoc.

Na razie mam klasy:
GUI z wczytanym obrazkiem planszy.
Checker abstrakcyjną która trzyma kolor bierki, jej pozycję, typ(kończy coś innego), ilość wykonanych przez nią ruchów, możliwe ruchy, wartość, ikonę i inne rzeczy nie istotne w pytaniu.
Dziedziczą po niej wszystkie bierki i implementują(na razie w zamierzeniu) metodę wyznaczenia możliwych ruchów.
Field zawierającą kolor, bierkę i pozycję.
Position zawiera 2 inty: file i rank.
Board zawiera statyczną tablicę Field[8][8].
Color enum z biały, czarny.
Player zawiera color, i ocenę tempa, materiału i pozycji.
Ale to jest cienkie, bo bicia w przelocie przy tym rozkładzie klas nie uda się zrobić.

0
  1. MVC - logika gry powinna być oderwana calkowicie od tego jak to jest potem wizualizowane
  2. Raczej nie ma jednego poprawnego rozwiązania, ale skoro pionek zna swoją pozycję to ta tablica Field[][] jest już zbędna w planszy. Poza tym legalność ruchów może sprawdzać tylko plansza bo nikt inny nie ma dostępu do pozostałych pionków.
0

Dzięki.
Field[][] właśnie po to było żeby znać położenie pozostałych, ale Twoje rozwiązanie jest lepsze.

Kontroler mam zrobić tak, że jak użytkownik zrobi ruch ro mam sprawdzić czy jest możliwy, zmienić w GUI i obserwatora ustawić w klasie Board do zmiany w ustawieniu bierek?

0

W sumie, to lepiej wywalić pozycję z bierki i trzymać to w Board?

1

@DużaTajemnicaWiedzy tak może być trochę łatwiej, ale pamiętaj o SRP ;) Bo zaraz się okaże że Board to jest taki God-object który zajmuje się wszystkim.

2

Postaram się nie programować faraonowo :)

2

Plansza przechowująca pozycje (i tylko pozycje) wszystkich bierek nie jest w pełni kompetentnym sędzią. Nie potrafi rozstrzygnąć czy dopuszczalna jest roszada, czy można bić w przelocie.

1

Bo zaraz się okaże że Board to jest taki God-object który zajmuje się wszystkim.

I to wcale nie byłoby złe.
„Wszystkim” w sensie logiki gry, bo już rysowanie powinno być całkowicie wydzielone.

Field zawierającą kolor, bierkę i pozycję.
Koloru pola nie musisz trzymać, raz że jest zdeterminowany to dwa że jest zupełnie nieistotny z punktu widzenia zasad gry. Tym niech się przejmuje funkcja rysująca.
Pozycji pola też nie musisz trzymać, bo tę określają indeksy do tablicy [8][8].
Unikaj powielania tych samych danych.

Plansza przechowująca pozycje (i tylko pozycje) wszystkich bierek nie jest w pełni kompetentnym sędzią.
No parę dodatkowych flag określających stan gry będzie potrzebne.

czy grafikę trzymać jakoś osobno i trzymać ikony(linki) w jakimś xmlu?
To nieistotne. Na razie zajmij się odzwierciedleniem mechaniki gry, ładna wizualizacja poczeka...

PS. Przydadzą się oficjalne zasady, żeby uniknąć wersji „szachy pana Tadzia”.

2
Azarien napisał(a):

Bo zaraz się okaże że Board to jest taki God-object który zajmuje się wszystkim.

I to wcale nie byłoby złe.

Byłoby.

Na mój gust, to mniej więcej tak:
Board ma kolekcję Fields i Pieces.
Field umie określić swoją pozycję i ma referencję do Piece, która na nim stoi.
Piece jest abstrakcyjna ma Name, MovesCounter i kolekcję StandardMoveRules, która jest nadpisywana w klasach potomnych.
StandardMoveRules składa się z obiektów StandardRule, które określają: MoveDirection (z enuma), MoveFieldsLimit (liczba), AttackDirection, AttackFieldsLimit (każde z tych czterech, to na koński wypadek kolekcja. ;)) oraz CanJumpOver.
Do tego Board ma History, które składa się z Move.
Move ma referencje do dwóch Field, dwóch Piece i określa swój Type (ruch, bicie, promocja).
Potrzebny jest jeszcze CustomRulesProvider opisujący możliwość wykonania niestandardowego ruchu, czyli: pierwszy ruch piona, bicie w przelocie i roszadę.

A do tego MoveValidator, który na podstawie Board, History, CustomRulesProvider, i danych żądanego ruchu określi, czy jest on możliwy. W tym celu zweryfikuje:

  1. czy ruch jest w ogóle poprawny dla bierki (najpierw przez CustomRulesProvider, a potem korzystając ze StandardMoveRules);
  2. czy nie występuje jakaś ograniczająca sytuacja na planszy (np. szach, albo próba roszady przez pole szachowane - to wszystko bazując na Board);
  3. i czy nie ma jakichś zaszłości historycznych zabraniających tego ruchu (chyba dotyczy to tylko roszady i ruchu królem).

Jeżeli ruch można wykonać to następnie ResultCalculator na podstawie obecnego Board i History stwierdza, czy jest mat, pat, remis, czy jeszcze coś innego.
MoveValidator i ResultCalculator to na wszelki wypadek powinny być strategie (np. po to, żeby łatwo można było podmienić zasady na turniejowe).
History i Move to chyba przypadkiem wyjdzie jakaś prosta realizacja Event Sourcingu, którym niektórzy się strasznie ostatnio podniecają. ;P

0

Dla uproszczenia planszę będę numerował od 0 do 63 (nie jako [8][8]). Położenie na planszy bardzo łatwo policzyć w postaci 8x8. Ale to już kwestia jak komu wygodniej.

Podszedłbym nieco inaczej. Pion posiada stan i hostorię, którą dziedziczy po jakimś abstract pionie (na tej podstawie można łatwo określać czy pion może wyknać startowe ruchy, roszadę etc). Każdy pion dostaje Strategię ruchu. Czyli pion używając strategii wie gdzie i czy może sie ruszyć.
Board posiada tablicę AbstractPion[64]. Idea board jest po to, aby można było łatwo określać czy jakieś pole jest puste czy zajęte, dostać kolekcję wszystkich pionów białych czarnych etc. Sam board to tylko "worek" na dane
Strategia ruchu potrafi powiedzieć gdzie pion może się poruszyć(lista pól), oraz potrafi wykonać ruch.
Dodatkowo wygodny będzie obiekt Move(from,to,ref do piona) który jest konstruowany w fazie sprawdzania ruchów

Całość opakowana jest w obiekt np GameState który pośredniczy między interfejsem a grą. GameState posiada takie ifno jak akutalna tura, obiekt board, historię etc. Ten obiekt potrafi określać całkowity stan planszy (tj szach, mat, pat)

Na początku każdej tury interfejs odpytuje GS o możliwe ruchy do wykonania -> iterowanie po wszystkich pionach danego koloru w celu zebrania pionków które w ogóle mogą się ruszyć, oraz gdzie mogą się ruszyć, czyli zbudowanie kolekcji Move[].

czyli:
AbstractPion{
historia;
stan(kolor,zbuty,aktywny);
aktualnaPozycja;
moveStrategy; <- to zależy od sposobu w jaki pion używa stratergii, czy przez repozytorium czy dostaje w trakcie konstrukcji, ewentualnie czy board rusza pionami
---
Move[] getPossibleMoves(Board board);
void move(Board board);
void afterMove(); <- metoda wykonywana przez strategię po wykonaniu ruchy w celu określania np. promocji
}

Board{
AbstractPion[64]
---
AbstractPion[] getWhitePawns();
AbstractPion[] getBlackPawns();
Move[] getPossibleMove(Color);
}
GameState{
turn;
hitedPawns;
Board;
doneMoves;
---
Move[] getPossibleMove();
void move(int from, int to);
void changeTurn();
State checkState();
}
MoveStrategy<T extend AbstractPawns>{
Move[] getPossibleMoves(T pawn, GameBoard board);
move(t pawn, GameBoard board);
}
(Same strategie ruchów są bezstanowe i mogą być przekazywane pionom w trakcie konsstrukcji bądź piony będą się odwoływać do jakiegoś repozytorium strategi, co wygodniej). Sam interfejs strategi w takiej postaci jest o tyle wygodny, że bardzo łatwo pisać testy jednostkowe dla poszczególnych rodzajów pionów. Interfejs strategii może być generyczny aby ustszec się sytuacji kiedy Stretegię piona poda się wieży. Innym rozwiązaniemm jest iterowanie przez piony w klasie board i to board wykonuje metody strategii, tak że pion nie będzie wiedział o istnieniu strategii. W sumie do przemyślenia.

Metoda checkState może się przeiterować po pionach i na bazie ich strategii określać czy ktoś nie atakuje króla.
Każdy wykonany ruch (Move) odkładamy do listy done moves - aby określić ewentualny remis.


odpowiadając na pytanie Somekind:
coś w rodzaju:
board:
for(Pawn p : pawns){
p.getPossibleMove(this);
}

pawn:
getPossibleMove(Board){
strategy.getPossibleMove(this, board);
}

inne rozwiązanie:
board:
for(Pawn p : pawns){
RepozytoriumStrategii.getStrategyForPawn(p).move(p, this)
}

1

Dla uproszczenia planszę będę numerował od 0 do 63 (nie jako [8][8]).
No i tak bardzo uprościłeś, że ruch o pole „do przodu” to ruch o 8, o dwa pola po skosie to ruch o 18 itd.
Moim zdaniem bez sensu.

Niesamowicie panowie komplikujecie zadanie ;-)
Jakieś strategie, walidatory, providery...

To jest gra w szachy. Zasady ruchów są zamknięte – nigdy nie będzie nowych bierek, nowych ruchów. A jeśli będą, to nie będą to już szachy.

Wszystko może być spokojnie hardcodowane, architektura naprawdę nie musi być otwarta na nowe „strategie” czy zewnętrzne komponenty.

Całe te wasze strategie można uprościć do

switch(rodzajBierki)
{
  case pionek: ...
  case wieża: ...
  case goniec: ...
  itd.
}

Zaraz posypią się minusy. Ale w programowaniu nie chodzi o stosowanie na siłę wzorców, dziedziczenia, obiektowości – tylko o wykonanie zadania.

Ja klasę widzę jedną „biznesową” do samej gry (nazwijmy ją Chess) i drugą - kontrolkę wizualną, powiedzmy Chessboard.
Ewentualnie jeszcze jakieś Player — jeśli gra ma być przez sieć albo zawierać AI, to nad tym też trzeba się zastanowić.

Ale na pewno nie osobna klasa dla każdej bierki czy pola na szachownicy.

2

@Azarien rozumiem że dla ciebie "wzorce" i "dekompozycja kodu" to są tylko jakieś akademickie banialuki i preferujesz pisanie kodu "na pałę"? Przecież użycie osobnej klasy dla każdej bierki ma służyć uproszczeniu kodu a nie jego zagmatwaniu! Dzieki temu wywołasz metodę na bierce i wykona sie ta metoda która powinna. Ale rozumiem że to wolisz 100 razy zahardkodować takiego pięknego switcha?
Przecież idea jest taka żeby mieć małe i proste klasy w których łatwo szuka sie blędów. A ty chcesz zrobić god-object który będzie miał ~kilka tysiecy linii kodu, zapewne wszystko w jednej/dwóch metodach. Życzę powodzenia jak sie okaże że trzeba to debuggować ;)

0

@Azarien - zasadniczo dobre spostrzeżenie, ale przeginasz IMO w drugą stronę.

No i tak bardzo uprościłeś, że ruch o pole „do przodu” to ruch o 8, o dwa pola po skosie to ruch o 18 itd.

Tu akurat masz pełną rację, tak się kończy za dużo programowania niskopoziomowego ;P. Plansza to tablica dwuwumiarowa jak by nie patrzeć.

To jest gra w szachy. Zasady ruchów są zamknięte – nigdy nie będzie nowych bierek, nowych ruchów. A jeśli będą, to nie będą to już szachy.

No ruchów zasadniczo nowych nie będzie, ale co jeśli przyjdzie komuś do głowy sterowany komputerowo przeciwnik? Podstawową zasadą jest to że nigdy nie wiadomo co się zmieni ;P Może się okazać że mimo że zasady gry się nie zmienią trzeba będzie coś modyfikować...

Tak czy inaczej, nawet jeśli redukujemy złożoność z poziomu klas do poziomu metod to i tak trzeba to trochę przemyśleć.

Na przykład żeby nie powstały metody na 300 linijek kodu z obsługą wszystkich możliwych typów pionków).
Nie możesz sobie wrzucić wszystkiego do jednej metody bo to /nie/ będzie czytelne.
Edit: w sumie mając takie wielkie klasy jest jeszcze jeden ciekawy problem - jak to unittestować?

Albo jeśli walidacja ruchów będzie następowała przy próbie przesunięcia bierki to co jeśli np. będzie potrzeba pokolorowania wszystkich pól na które pionek się może ruszyć (dość standardowe wymaganie)? Tutaj by się raczej przydała metoda do zwracania dla każdego typu pionka listy pól na które się może ruszyć.
Niech już będzie że metoda a nie strategia, chociaż nie koniecznie dużo klas = źle (6 klas z 30 linijkami kodu jest lepsze niż jedna faraonowa).

Co do klas, przyda się też na pewno klasa dla pionka przechowująca pozycję, kolor i typ, enum z kolorem, i takie drobnostki. Z większych, dorzuciłbym coś pośredniczącego pomiędzy biznesową Chess i wizualną Chessboard (Chess ma metody typu przesuńBierkę, zbijBierkę() a Chessboard otrzymuje informacje o pojedynczych kliknięciach myszy. A pomiędzy tym jest stan, bo na przykład naciśnięcia na bierkę myszą może oznaczać 1) wybranie tej bierki 2) zbicie innej bierki (jeśli wybrana jest już bierka biała/czarna a wskazana jest czarna/biała) 3) roszadę, itd. Wrzucenie tego wszystkiego do Chessboard to nie jest dobry pomysł, zaśmiecanie Chess sposobami wybierania ruchu też nie za bardzo).

0
Shalom napisał(a)

@Azarien rozumiem że dla ciebie "wzorce" i "dekompozycja kodu" to są tylko jakieś akademickie banialuki i preferujesz pisanie kodu "na pałę"?
Pisanie kodu czytelnego, w którym wiadomo gdzie co jest, który widać jak działa, w którym nieznający kodu łatwo się odnajdzie.

Przecież użycie osobnej klasy dla każdej bierki ma służyć uproszczeniu kodu a nie jego zagmatwaniu
W teorii. W praktyce często wychodzi odwrotnie...

Ale rozumiem że to wolisz 100 razy zahardkodować takiego pięknego switcha?
Dlaczego 100?

MSM napisał(a)

zasadniczo dobre spostrzeżenie, ale przeginasz IMO w drugą stronę.
Dziękuję. Przeciwwaga w dyskusji zawsze dobra :-)

No ruchów zasadniczo nowych nie będzie, ale co jeśli przyjdzie komuś do głowy sterowany komputerowo przeciwnik
Dla klasy Chess nie powinno mieć znaczenia jak jest sterowany gracz, bo do gracza będzie osobna klasa.

Może się okazać że mimo że zasady gry się nie zmienią trzeba będzie coś modyfikować...
Pewnie że będzie. Przecież nie mówimy o ryciu kodu w kamieniu.

Edit: w sumie mając takie wielkie klasy jest jeszcze jeden ciekawy problem - jak to unittestować?
Nie widzę różnicy.
Załóżmy, że testujemy ruchy poszczególnych bierek. Jeśli każda jest w osobnej klasie - testujemy wirtualne metody tych klas. Jeśli ruchy są w jednej klasie ale 8 osobnych metodach, testujemy osiem metod. Jeśli w jednej metodzie - odpalamy tę metodę odpowiednią ilość razy z właściwymi parametrami.
Testów i tak wychodzi tyle samo.

Zresztą czy coś rozbijać na osobne metody czy zlepić w jedną – to często wychodzi w praniu, nie zawsze możemy przewidzieć jak duża metoda nam wyjdzie.

co jeśli np. będzie potrzeba pokolorowania wszystkich pól na które pionek się może ruszyć (dość standardowe wymaganie)?
Klasa Chessboard pyta klasę Chess, jakie ruchy można daną bierką wykonać.

Tutaj by się raczej przydała metoda do zwracania dla każdego typu pionka listy pól na które się może ruszyć.
Oczywiście.

Chess ma metody typu przesuńBierkę, zbijBierkę() a Chessboard otrzymuje informacje o pojedynczych kliknięciach myszy.
Widzę że rozumiesz mój zamysł.

A pomiędzy tym jest stan, bo na przykład naciśnięcia na bierkę myszą może oznaczać 1) wybranie tej bierki 2) zbicie innej bierki
I tak należałoby się zastanowić, po której stronie ten stan trzymać (nawet jeśli jako osobną klasę czy strukturę). Bo część stanu faktycznie obie klasy muszą znać.

0

No oczywiście, że wszystko można spieprzyć. Pytanie tylko, czy warto? Kod każdej dowolnej aplikacji można zamknąć w statycznych klasach ze statycznymi metodami, a do przetestowania dać klientowi, bo żadnych automatycznych testów do tego się nie zrobi.

W szachach być może nic się nie zmieni, ale wystarczy, że zmienią się nasze potrzeby. Co, jeśli będziemy chcieli wprowadzić rozgrywkę z zegarem? Dla mnie to podmiana ResultCalculator na taki, który mierzy czas. Przy podejściu "prostym", pewno skończy się na dopisaniu kolejnej drabinki ifów.

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