Я думаю, що тут проблема полягає в тому, що ви не дали чіткого опису, з якими завданнями обробляються які класи. Я опишу те, що, на мою думку, є хорошим описом того, що повинен робити кожен клас, тоді наведу приклад загального коду, який ілюструє ідеї. Ми побачимо, що код менш сполучений, і тому він насправді не має кругових посилань.
Почнемо з опису того, що робить кожен клас.
GameState
Клас повинен містити тільки інформацію про поточний стан гри. Він не повинен містити будь-якої інформації про те, які минулі стани гри чи які можливі майбутні рухи. Він повинен містити лише інформацію про те, які фігури є, на яких квадратах у шахах, чи скільки та який тип шашок, на яких точках у нардах. GameState
Міститиме деяку додаткову інформацію, як інформація про рокіровку в шахах або про подвоєнні куба в нарди.
Move
Клас трохи складніше. Я б сказав, що я можу вказати хід гри, вказавши, GameState
що є результатом гри. Тож ви могли собі уявити, що такий крок можна просто здійснити як GameState
. Однак, в ході (наприклад,) ви можете уявити, що набагато простіше вказати хід, вказавши одну точку на дошці. Ми хочемо, щоб наш Move
клас був достатньо гнучким, щоб впоратися з будь-яким із цих випадків. Тому Move
клас насправді буде інтерфейсом із методом, який здійснює попередній хід GameState
і повертає новий пост-хідGameState
.
Тепер RuleBook
клас відповідає за те, щоб знати все про правила. Це можна розбити на три речі. Потрібно знати, що таке початковеGameState
, він повинен знати, які ходи є законними, і потрібно вміти розповідати, чи виграв хтось із гравців.
Ви також можете зробити GameHistory
клас, щоб відслідковувати всі зроблені кроки та все, GameStates
що відбулося. Новий клас необхідний, тому що ми вирішили, що сингл GameState
не повинен нести відповідальність за те GameState
, що він знає все , що було раніше.
На цьому закінчуються класи / інтерфейси, які я обговорюватиму. У вас також єBoard
клас. Але я думаю, що дошки в різних іграх досить різні, що важко зрозуміти, що можна зробити з дошками загалом. Зараз я продовжуватиму давати загальні інтерфейси та реалізовувати загальні класи.
По-перше, це GameState
. Оскільки цей клас повністю залежить від конкретної гри, немає загального Gamestate
інтерфейсу чи класу.
Далі є Move
. Як я вже говорив, це можна представити інтерфейсом, який має єдиний метод, який приймає стан перед переміщенням і створює стан після переміщення. Ось код цього інтерфейсу:
package boardgame;
/**
*
* @param <T> The type of GameState
*/
public interface Move<T> {
T makeResultingState(T preMoveState) throws IllegalArgumentException;
}
Зауважте, що є параметр типу. Це тому, що, наприклад, ChessMove
потрібно буде знати про деталі передпереїзду ChessGameState
. Так, наприклад, оголошення класу ChessMove
буде
class ChessMove extends Move<ChessGameState>
,
де ви б уже визначили ChessGameState
клас.
Далі я обговорюю загальний RuleBook
клас. Ось код:
package boardgame;
import java.util.List;
/**
*
* @param <T> The type of GameState
*/
public interface RuleBook<T> {
T makeInitialState();
List<Move<T>> makeMoveList(T gameState);
StateEvaluation evaluateState(T gameState);
boolean isMoveLegal(Move<T> move, T currentState);
}
Знову є параметр типу для GameState
класу. Оскільки RuleBook
повинен знати, що таке початковий стан, ми застосували метод надання початкового стану. Оскільки RuleBook
припущення повинні знати, які кроки є законними, у нас є методи перевірити, чи є перехід легальним у даному стані, та надати перелік законних кроків для даного стану. Нарешті, існує метод оцінювання GameState
. Помітьте, що RuleBook
відповідальність за опис має бути лише в тому випадку, якщо той чи інший гравець вже виграв, але не той, хто знаходиться в кращому становищі посеред гри. Вирішити, хто в кращому становищі - це складна річ, яку слід перенести у свій клас. Тому StateEvaluation
клас насправді є просто простим перерахунком, наведеним нижче:
package boardgame;
/**
*
*/
public enum StateEvaluation {
UNFINISHED,
PLAYER_ONE_WINS,
PLAYER_TWO_WINS,
DRAW,
ILLEGAL_STATE
}
Нарешті, опишемо GameHistory
клас. Цей клас відповідає за запам'ятовування всіх позицій, які були досягнуті в грі, а також рухи, які були відтворені. Головне, що він повинен вміти - це записати Move
як зіграний. Ви також можете додати функціональність для скасування Move
s. У мене є реалізація нижче.
package boardgame;
import java.util.ArrayList;
import java.util.List;
/**
*
* @param <T> The type of GameState
*/
public class GameHistory<T> {
private List<T> states;
private List<Move<T>> moves;
public GameHistory(T initialState) {
states = new ArrayList<>();
states.add(initialState);
moves = new ArrayList<>();
}
void recordMove(Move<T> move) throws IllegalArgumentException {
moves.add(move);
states.add(move.makeResultingState(getMostRecentState()));
}
void resetToNthState(int n) {
states = states.subList(0, n + 1);
moves = moves.subList(0, n);
}
void undoLastMove() {
resetToNthState(getNumberOfMoves() - 1);
}
T getMostRecentState() {
return states.get(getNumberOfMoves());
}
T getStateAfterNthMove(int n) {
return states.get(n + 1);
}
Move<T> getNthMove(int n) {
return moves.get(n);
}
int getNumberOfMoves() {
return moves.size();
}
}
Нарешті, ми могли б уявити собі Game
клас, щоб зв'язати все разом. Цей Game
клас повинен розкривати методи, які дозволяють людям бачити, що таке струм GameState
, бачити, хто, якщо хто має його, бачити, які рухи можна грати, і грати в рух. У мене є реалізація нижче
package boardgame;
import java.util.List;
/**
*
* @author brian
* @param <T> The type of GameState
*/
public class Game<T> {
GameHistory<T> gameHistory;
RuleBook<T> ruleBook;
public Game(RuleBook<T> ruleBook) {
this.ruleBook = ruleBook;
final T initialState = ruleBook.makeInitialState();
gameHistory = new GameHistory<>(initialState);
}
T getCurrentState() {
return gameHistory.getMostRecentState();
}
List<Move<T>> getLegalMoves() {
return ruleBook.makeMoveList(getCurrentState());
}
void doMove(Move<T> move) throws IllegalArgumentException {
if (!ruleBook.isMoveLegal(move, getCurrentState())) {
throw new IllegalArgumentException("Move is not legal in this position");
}
gameHistory.recordMove(move);
}
void undoMove() {
gameHistory.undoLastMove();
}
StateEvaluation evaluateState() {
return ruleBook.evaluateState(getCurrentState());
}
}
Зауважте в цьому класі, що RuleBook
не несе відповідальності за знання того, що таке струм GameState
. Це GameHistory
робота. Тож Game
запитує, GameHistory
що таке сучасний стан, і дає цю інформацію тим часом, RuleBook
коли Game
потрібно сказати, які правові кроки є чи хтось переміг.
У будь-якому разі, суть цієї відповіді полягає в тому, що після того, як ви зробили розумне визначення того, за що відповідає кожен клас, і ви робите кожен клас зосередженим на невеликій кількості обов'язків, і ви присвоюєте кожну відповідальність унікальному класу, то класи як правило, роз'єднується, і все легко кодується. Сподіваюсь, це видно з прикладів коду, які я дав.
RuleBook
взяти, наприкладState
, аргумент, і повернути дійснеMoveList
, тобто "ось де ми зараз, що можна зробити далі?"