web-dev-qa-db-ja.com

OOP原則を使用して、チェスゲームのさまざまな部分のさまざまな動きを設計する方法は?

ゲームの各部分が_abstract Piece_クラスから継承するチェスゲームを設計しようとしています。作品はいくつかのことを行います:

  • それはそれがどの色であるかを示します(白/黒)
  • それはそれがどのタイプかを教えてくれます
  • 移動が有効かどうかを通知します
  • そしておそらくそれが現在あるボード上のセル/正方形

以前、私は検証をピース自体の一部にすることを考えていました。これにより、各ピースが実装する必要があるabstract validateMove()メソッドが存在しますが、次の場合、これによりコードが大幅に変更される可能性があります。将来、特定のピースの移動検証メカニズムを変更することにします。これは、Head First Designの本に記載されているflyメソッドがないRubber duckの場合に似ています。

したがって、私はこれを作成することを考えましたインターフェイスと呼ばれるMoveValidator、これはピース固有の実装を持ち、各ピースはバリデーター自体ではなく変数の1つとしてバリデーターを持ちますまたは検証自体を実装します。

したがって、Pawnはその構成の1つとしてPawnMoveValidatorを持ちます。

だから私は次の抽象的なピースクラスを持っています

_@RequiredArgsConstructor
public abstract class Piece {

    @NonNull
    @Getter
    private final PieceColor pieceColor;

    @NonNull
    @Getter
    private final MoveValidator moveValidator;

    @Getter
    @Setter
    private Point currentLocation;


    private boolean basicValidation(Cell[][] board, Point src, Point dest) {
        .........
        .........
    }


    public boolean validateMove(Cell[][] board, Point src, Point dest) {
        return basicValidation(board, src, dest) && moveValidator.validateMove(board, src, dest);
    }

}
_

Cell.Java

_@RequiredArgsConstructor
public class Cell {

    @NonNull
    @Getter
    private final CellColor cellColor;

    @NonNull
    @Getter
    private final Integer row;

    @NonNull
    @Getter
    private final Integer col;
}
_

Point.Java

_@RequiredArgsConstructor
public class Point {

    @NonNull
    @Getter
    private final Integer x;

    @NonNull
    @Getter
    private final Integer y;
}
_

MoveValidator.Java

_public interface MoveValidator {

    validateMove(Cell[][] board, Point src, Point dest);
}
_

ここでいくつか質問があります。

  1. これは良いデザインですか、それともさらに改善できますか?
  2. Pieceに実装する抽象メソッドがない場合でも、抽象クラスのままにすべきですか、それとも具象クラスになるべきですか?
  3. 特定のピースの移動検証で、それが実装されている特定のピースの状態の知識が必要な場合はどうなりますか?例:ポーンは2つのセルを前方に移動したいと考えています。 Pawnクラスには、それが最初の移動かどうかを示すフラグがあります。その場合、PawnMoveValidatorが移動を検証したい場合、フラグの状態を知る必要があります。 Pawnクラスにバリデーター自体を実装させずに、それをどのように行うことができますか?

御時間ありがとうございます

2
Auro

これは良いデザインですか、それともさらに改善できますか?

私にとって、デザインは常に改善することができます。少なくとも、現在のデザインでは解決できないムーブメントの問題がいくつかあります。

ピースに実装する抽象メソッドがない場合でも、それは抽象クラスのままであるのか、それとも具象クラスになるのか?

クラスに抽象メソッドがない場合、インスタンス化できない原因は何ですか?一部のコードを再利用する場合は、コンポジションを使用します。いくつかのタイピングの制約を満たしたい場合は、インターフェースを使用してください。個人的には、抽象クラスがある場合にのみ抽象クラスを使用します。

特定のピースの移動検証で、それが実装されている特定のピースの状態の知識が必要な場合はどうなりますか?例:ポーンは2つのセルを前方に移動したいと考えています。 Pawnクラスには、それが最初の移動かどうかを示すフラグがあります。その場合、PawnMoveValidatorが移動を検証したい場合は、フラグの状態を知る必要があります。 Pawnクラスにバリデーター自体を実装させずに、それをどのように行うことができますか?

残念ながら、移動が有効かどうかを決定するプロセスには、多くのことが関与しています。

一部のルールには、ボードの現在の状態に関する情報が必要です。

  • ターゲットセルは現在のピースの可能な移動ですか?
  • ターゲットセルは空ですか?
  • 移動を妨げる他の部分はありますか?

いくつかのルールは、現在の部分とおそらく関連部分(キャスティング)に関する情報を必要とします:

  • これは駒の最初の動きですか(ポーン、キング、ルークの場合)?
  • ポーンは昇格されますか?

ご覧のとおり、移動とは1つのセルから別のセルに1つのピースを移動することだけではありません。キャスティングでは、1回の移動で2つの駒があります。

要約すると、現在のデザインは私には非常に明確です。ただし、すべてのルールを使用して完全なチェス盤をモデル化するには、さらに作業が必要になる場合があります。私はいくつかの提案があります:

  • 「移動」を別のクラス(またはクラスのファミリー)にする。
  • 移動バリデーターは、プライベートメンバーとしてBoardを持ち、Moveメソッドでvalidateオブジェクトを受け入れます。
1
Hieu Le

あなたはデザインを必要以上に複雑にしています。ボードは、駒ではなく移動の検証を処理します。これは、作品はそれがどんな種類の作品であるかを知っているだけだからです。それがどこにあるか、またはボードがどのように見えるかはわかりません。

どのようなメリットがありますか?コードは非常に単純になります。

  • ピースごとにクラスは必要ありません
  • MoveValidatorは必要ありません。短い関数です
  • Cellクラスは必要ありません
  • 移動するたびに更新する必要はありません。ボードを更新するだけです

場所は、作品に固有の品質ではありません。ボードとの関係でこの作品にのみ存在するものです。 ピースの場所を要求しないでください。ボード上の場所でピースを要求します。これは重要な違いです。

  • アイテムは捕獲されたときに場所がありません
  • チェスのルールでは、同じタイプと色のピースは同一であると想定しています。

チェスのゲームを終了する条件の1つは、チェス盤が同じ状態に3回入ることです。これは引き分けになります。

このルールを正しく実装する最も簡単な方法は、ボードの以前のすべての状態を追跡することです。これは思ったよりも簡単です。ボードのコピーを作成するのが簡単な場合は、移動する前にボードのコピーを保存するだけです。この条件をチェックするときは、現在の状態を以前の状態のリストと比較するだけです。

同じ色の2頭の馬がボード上の位置を切り替えたが、それ以外は何も変更されなかった場合、ボードは同じままです。つまり、最初からこれらの馬が異なるとは考えないでください。

まとめると、これはファイル数、コード行数が少なくなり、パフォーマンスが向上することを意味します。

次のようにコードを書き直すことができます。

public class Piece {
    public enum Color { 
        Black, White 
    }
    public enum Type {
        Pawn, King, Queen, Rook, Knight, Bishop
    }
    @NonNull
    @Getter
    private Color color;
    @NonNull
    @Getter
    private Type type;

    public Piece(Color color, Type type) {
        this.color = color;
        this.type = type;
    }
    public Piece(Piece p) {
        this.color = p.color;
        this.type = p.type;
    }

    @Override
    public boolean equals(Object o) {
        Piece p = (Piece)o;
        return p.color == color && p.type == type;
    }

    // Gets the point value of the piece
    public int pointValue() {
        switch(type) {
            case Pawn: return 1;
            case King: return 1000;
            case Queen: return 9;
            case Rook: return 5;
            case Knight: return 3;
            case Bishop: return 3;
        }
    }
    // Check if a move between two points is legal for the piece under
    // normal circumstances
    // X is the column, Y is the row. X and Y are from 0 to 7
    public boolean moveLegal(int startX, int startY, int endX, int endY) {
        int dx = endX - startX; 
        int dy = endY - startY;
        // If the color is black, flip how the board looks
        // So that the home row is always 0 and the row with pawns is 1
        if(color == Black) {
            startY = 7 - startY; 
        }

        // You can't move a piece off the board
        if(endX < 0 || endX > 7) return false;
        if(endY < 0 || endY > 7) return false;
        
        // Not moving a piece isn't a legal move
        if(dx == 0 && dy == 0) 
            return false; 

        // If the change in position is always positive it simplifies the math
        if(dx < 0) dx = -dx;
        if(dy < 0) dy = -dy;

        switch(type) {
            case Pawn:
                // A pawn can move forward 1, unless it's in the first row
                return dy == 1 || (dy == 2 && startY == 1);
            case Rook: 
                // It can move horizontally or vertically, but not both
                return (dx != 0 && dy == 0) || (dx == 0 && dy != 0);
            case Knight:
                // A night can do a diagonal hop
                return (dx == 1 && dy == 2) || (dx == 2 && dy == 1);
            case King:
                return dx == 1 || dy == 1;
            case Queen: 
                 return dx == 0 || dy == 0 || dx == dy; 
            case Bishop:
                 return dx == dy; 
        }
    }
}

ご覧のとおり、ピースはまだ移動方法を知っていますが、an MoveValidatorやその他の複雑なソリューションは必要ありません。ピースの種類ごとにクラスを分ける必要はありません。また、ピースが現在の場所であることを知るためのピースも必要ありません。ボードは、いくつかの単純なメンバー関数でそれを処理します。

public class Board {
    // Set up board in constructor
    Piece[][] pieces;
    private boolean bad(int index) {
        return index < 0 || index > 7;
    }
    public boolean isEmpty(int x, int y) {
        return pieces[x][y] == null; 
    }
    public boolean moveValid(int startX, int startY, int endX, int endY) {
    // If the move is off the board, it's invalid
       if(bad(startX) || bad(startY) || bad(endX) || bad(endY)) 
             return false;
   
        // You can't move where there is no piece
        if(isEmpty(startX, startY)) 
            return false;

        Piece piece = pieces[startX, startY];
        // Check that the endpoint doesn't contain a piece of the same color
        if(!isEmpty(endX, endY))
            if(pieces[endX][endY].getColor() == piece.getColor())
                return false; 

        // If the piece can't move that way, it's not valid
        if(!piece.moveLegal(startX, startY, endX, endY))
            return false; 
        // To-do: check that cells in-between are empty
    }
}        
1