Creo que el problema aquí es que no ha dado una descripción clara de qué tareas deben ser manejadas por qué clases. Describiré lo que creo que es una buena descripción de lo que debe hacer cada clase, luego daré un ejemplo de código genérico que ilustra las ideas. Veremos que el código está menos acoplado, por lo que en realidad no tiene referencias circulares.
Comencemos describiendo lo que hace cada clase.
La GameState
clase solo debe contener información sobre el estado actual del juego. No debe contener ninguna información sobre cuáles son los estados pasados del juego o qué movimientos futuros son posibles. Solo debe contener información sobre qué piezas están en qué cuadros del ajedrez, o cuántos y qué tipo de fichas están en qué puntos del backgammon. El GameState
tendrá que contener alguna información adicional, como información sobre el enroque en el ajedrez o alrededor de la duplicación del cubo en el backgammon.
La Move
clase es un poco complicada. Yo diría que puedo especificar un movimiento para jugar especificando el GameState
resultado de jugar el movimiento. Así que podrías imaginar que un movimiento solo se puede implementar como a GameState
. Sin embargo, en go (por ejemplo) podría imaginar que es mucho más fácil especificar un movimiento especificando un solo punto en el tablero. Queremos que nuestra Move
clase sea lo suficientemente flexible como para manejar cualquiera de estos casos. Por lo tanto, la Move
clase realmente será una interfaz con un método que toma un movimiento previo GameState
y devuelve un nuevo movimiento posterior GameState
.
Ahora la RuleBook
clase es responsable de saber todo sobre las reglas. Esto se puede dividir en tres cosas. Necesita saber cuál es la inicial GameState
, necesita saber qué movimientos son legales y necesita saber si uno de los jugadores ha ganado.
También puede hacer una GameHistory
clase para realizar un seguimiento de todos los movimientos que se han realizado y todo lo GameStates
que ha sucedido. Es necesaria una nueva clase porque decidimos que una sola GameState
no debería ser responsable de conocer todos los GameState
mensajes anteriores.
Esto concluye las clases / interfaces que discutiré. También tienes una Board
clase. Pero creo que los tableros en diferentes juegos son lo suficientemente diferentes como para que sea difícil ver qué podría hacerse genéricamente con los tableros. Ahora voy a dar interfaces genéricas e implementar clases genéricas.
En primer lugar es GameState
. Dado que esta clase depende completamente del juego en particular, no existe una Gamestate
interfaz o clase genérica .
El siguiente es Move
. Como dije, esto se puede representar con una interfaz que tiene un único método que toma un estado previo al movimiento y produce un estado posterior al movimiento. Aquí está el código para esta interfaz:
package boardgame;
/**
*
* @param <T> The type of GameState
*/
public interface Move<T> {
T makeResultingState(T preMoveState) throws IllegalArgumentException;
}
Tenga en cuenta que hay un parámetro de tipo. Esto se debe a que, por ejemplo, ChessMove
será necesario conocer los detalles del pre-movimiento ChessGameState
. Entonces, por ejemplo, la declaración de clase de ChessMove
sería
class ChessMove extends Move<ChessGameState>
,
donde ya habrías definido una ChessGameState
clase.
A continuación hablaré sobre la RuleBook
clase genérica . Aquí está el código:
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);
}
Nuevamente hay un parámetro de tipo para la GameState
clase. Como RuleBook
se supone que sabe cuál es el estado inicial, hemos puesto un método para dar el estado inicial. Dado que RuleBook
se supone que sabe qué movimientos son legales, tenemos métodos para probar si un movimiento es legal en un estado determinado y para dar una lista de movimientos legales para un estado determinado. Finalmente, hay un método para evaluar el GameState
. Tenga en cuenta que el RuleBook
solo debe ser responsable de describir si uno u otro jugador ya ha ganado, pero no quién está en una mejor posición en medio de un juego. Decidir quién está en una mejor posición es algo complicado que debería trasladarse a su propia clase. Por lo tanto, la StateEvaluation
clase es en realidad solo una enumeración simple dada de la siguiente manera:
package boardgame;
/**
*
*/
public enum StateEvaluation {
UNFINISHED,
PLAYER_ONE_WINS,
PLAYER_TWO_WINS,
DRAW,
ILLEGAL_STATE
}
Por último, describamos la GameHistory
clase. Esta clase es responsable de recordar todas las posiciones que se alcanzaron en el juego, así como los movimientos que se jugaron. Lo principal que debería poder hacer es grabar una Move
reproducción. También puede agregar funcionalidad para deshacer Move
s. Tengo una implementación a continuación.
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();
}
}
Finalmente, podríamos imaginar hacer una Game
clase para unir todo. Se Game
supone que esta clase expone métodos que permiten a las personas ver cuál es la corriente GameState
, ver quién, si alguien tiene uno, ver qué movimientos se pueden jugar y jugar un movimiento. Tengo una implementación a continuación
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());
}
}
Observe en esta clase que RuleBook
no es responsable de saber cuál es la corriente GameState
. Ese es el GameHistory
trabajo de. Entonces, Game
pregunta GameHistory
cuál es el estado actual y le da esta información a RuleBook
cuándo Game
necesita decir cuáles son los movimientos legales o si alguien ha ganado.
De todos modos, el punto de esta respuesta es que una vez que haya hecho una determinación razonable de lo que cada clase es responsable, y hace que cada clase se centre en un pequeño número de responsabilidades, y asigne cada responsabilidad a una clase única, entonces las clases tienden a estar desacoplados, y todo se vuelve fácil de codificar. Esperemos que sea evidente por los ejemplos de código que di.
RuleBook
tomamos, por ejemplo, elState
como argumento, y devolvimos el válidoMoveList
, es decir, "aquí es donde estamos ahora, qué se puede hacer a continuación?"