Implemented proper pawn promotion

* Moved move execution and reversion to the Move class
* Removed Move.Type enumeration
* Added Move subclasses Castling, EnPassant and PawnPromotion
* Generating all four possible pawn promotions in the Pawn class
* Temporarily removed special move support from NaturalPlayer
This commit is contained in:
Kai S. K. Engelbart 2019-11-03 15:46:08 +01:00
parent b79c592c67
commit 2821f30dbe
Signed by: kske
GPG Key ID: 8BEB13EC5DF7EF13
8 changed files with 189 additions and 162 deletions

View File

@ -61,9 +61,6 @@ public class Board {
Piece piece = getPos(move);
if (piece == null || !piece.isValidMove(move)) return false;
else {
// Set type after validation
if (move.getType() == Move.Type.UNKNOWN) move.type = Move.Type.NORMAL;
// Move piece
move(move);
@ -86,37 +83,8 @@ public class Board {
Piece piece = getPos(move);
Piece capturePiece = getDest(move);
switch (move.getType()) {
case PAWN_PROMOTION:
setPos(move, null);
// TODO: Select promotion
setDest(move, new Queen(piece.getColor(), this));
break;
case EN_PASSANT:
setDest(move, piece);
setPos(move, null);
boardArr[move.getDest().x][move.getDest().y - move.getySign()] = null;
break;
case CASTLING:
// Move the king
setDest(move, piece);
setPos(move, null);
// Move the rook
Move rookMove = move.getDest().x == 6 ? new Move(7, move.getPos().y, 5, move.getPos().y) // Kingside
: new Move(0, move.getPos().y, 3, move.getPos().y); // Queenside
setDest(rookMove, getPos(rookMove));
setPos(rookMove, null);
break;
case UNKNOWN:
System.err.printf("Move of unknown type %s found!%n", move);
case NORMAL:
setDest(move, piece);
setPos(move, null);
break;
default:
System.err.printf("Move %s of unimplemented type found!%n", move);
}
// Execute the move
move.execute(this);
// Update the king's position if the moved piece is the king
if (piece.getType() == Type.KING) kingPos.put(piece.getColor(), move.getDest());
@ -136,15 +104,15 @@ public class Board {
Pattern.compile(
"^(?<pieceType>[NBRQK])(?:(?<fromFile>[a-h])|(?<fromRank>[1-8])|(?<fromSquare>[a-h][1-8]))?x?(?<toSquare>[a-h][1-8])(?:\\+{0,2}|\\#)$"));
patterns.put("pawnCapture",
Pattern.compile("^(?<fromFile>[a-h])(?<fromRank>[1-8])?x(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQK])?(?:\\+{0,2}|\\#)?$"));
patterns.put("pawnPush", Pattern.compile("^(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQK])?(?:\\+{0,2}|\\#)$"));
Pattern.compile("^(?<fromFile>[a-h])(?<fromRank>[1-8])?x(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)?$"));
patterns.put("pawnPush", Pattern.compile("^(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)$"));
patterns.put("castling", Pattern.compile("^(?<queenside>O-O-O)|(?<kingside>O-O)(?:\\+{0,2}|\\#)?$"));
patterns.forEach((patternName, pattern) -> {
Matcher m = pattern.matcher(sanMove);
if (m.find()) {
Position pos = null, dest = null;
Move.Type moveType = Move.Type.NORMAL;
Position pos = null, dest = null;
Move move = null;
switch (patternName) {
case "pieceMove":
dest = Position.fromLAN(m.group("toSquare"));
@ -163,12 +131,16 @@ public class Board {
pos = Position.fromLAN(String.format("%c%d", file, rank));
} else pos = get(type, dest);
}
move = new Move(pos, dest);
break;
case "pawnCapture":
dest = Position.fromLAN(m.group("toSquare"));
char file = m.group("fromFile").charAt(0);
int rank = m.group("fromRank") == null ? get(Type.PAWN, file) : Integer.parseInt(m.group("fromRank"));
pos = Position.fromLAN(String.format("%c%d", file, rank));
if (m.group("promotedTo") != null) {
}
break;
case "pawnPush":
dest = Position.fromLAN(m.group("toSquare"));
@ -183,10 +155,10 @@ public class Board {
case "castling":
pos = new Position(4, log.getActiveColor() == Color.WHITE ? 7 : 0);
dest = new Position(m.group("kingside") != null ? 6 : 2, pos.y);
moveType = Move.Type.CASTLING;
move = new Castling(pos, dest);
break;
}
move(new Move(pos, dest, moveType));
move(move);
return;
}
});
@ -196,40 +168,11 @@ public class Board {
* Reverts the last move and removes it from the log.
*/
public void revert() {
MoveNode moveNode = log.getLast();
Move move = moveNode.move;
Piece capturedPiece = moveNode.capturedPiece;
MoveNode moveNode = log.getLast();
Move move = moveNode.move;
switch (move.getType()) {
case PAWN_PROMOTION:
setPos(move, new Pawn(getDest(move).getColor(), this));
setDest(move, capturedPiece);
break;
case EN_PASSANT:
setPos(move, getDest(move));
setDest(move, null);
boardArr[move.getDest().x][move.getDest().y - move.getySign()] = new Pawn(getPos(move).getColor().opposite(), this);
break;
case CASTLING:
// Move the king
setPos(move, getDest(move));
setDest(move, null);
// Move the rook
Move rookMove = move.getDest().x == 6 ? new Move(5, move.getPos().y, 7, move.getPos().y) // Kingside
: new Move(3, move.getPos().y, 0, move.getPos().y); // Queenside
setDest(rookMove, getPos(rookMove));
setPos(rookMove, null);
break;
case UNKNOWN:
System.err.printf("Move of unknown type %s found!%n", move);
case NORMAL:
setPos(move, getDest(move));
setDest(move, capturedPiece);
break;
default:
System.err.printf("Move %s of unimplemented type found!%n", move);
}
// Revert the move
move.revert(this, moveNode.capturedPiece);
// Update the king's position if the moved piece is the king
if (getPos(move).getType() == Type.KING) kingPos.put(getPos(move).getColor(), move.getPos());

View File

@ -0,0 +1,35 @@
package dev.kske.chess.board;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Castling.java</strong><br>
* Created: <strong>2 Nov 2019</strong><br>
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class Castling extends Move {
private final Move rookMove;
public Castling(Position pos, Position dest) {
super(pos, dest);
rookMove = dest.x == 6 ? new Move(7, pos.y, 5, pos.y) : new Move(0, pos.y, 3, pos.y);
}
public Castling(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); }
@Override
public void execute(Board board) {
// Move the king and the rook
super.execute(board);
rookMove.execute(board);
}
@Override
public void revert(Board board, Piece capturedPiece) {
// Move the king and the rook
super.revert(board, capturedPiece);
rookMove.revert(board, null);
}
}

View File

@ -0,0 +1,35 @@
package dev.kske.chess.board;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>EnPassant.java</strong><br>
* Created: <strong>2 Nov 2019</strong><br>
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class EnPassant extends Move {
private final Position capturePos;
public EnPassant(Position pos, Position dest) {
super(pos, dest);
capturePos = new Position(dest.x, dest.y - ySign);
}
public EnPassant(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); }
@Override
public void execute(Board board) {
super.execute(board);
board.set(capturePos, null);
}
@Override
public void revert(Board board, Piece capturedPiece) {
super.revert(board, capturedPiece);
board.set(capturePos, new Pawn(board.get(pos).getColor().opposite(), board));
}
public Position getCapturePos() { return capturePos; }
}

View File

@ -17,18 +17,9 @@ public class King extends Piece {
@Override
public boolean isValidMove(Move move) {
// Castling
if (move.getxDist() == 2 && move.getyDist() == 0) {
if (canCastleKingside()) {
move.type = Move.Type.CASTLING;
return true;
}
if (canCastleQueenside()) {
move.type = Move.Type.CASTLING;
return true;
}
}
return move.getxDist() <= 1 && move.getyDist() <= 1 && checkDestination(move);
return (move.getxDist() == 2 && move.getyDist() == 0
&& (move.getDest().x == 6 && canCastleKingside() || move.getDest().x == 2 && canCastleQueenside()))
|| move.getxDist() <= 1 && move.getyDist() <= 1 && checkDestination(move);
}
@Override
@ -42,8 +33,8 @@ public class King extends Piece {
}
// Castling
if (canCastleKingside()) moves.add(new Move(pos, new Position(6, pos.y), Move.Type.CASTLING));
if (canCastleQueenside()) moves.add(new Move(pos, new Position(2, pos.y), Move.Type.CASTLING));
if (canCastleKingside()) moves.add(new Castling(pos, new Position(6, pos.y)));
if (canCastleQueenside()) moves.add(new Castling(pos, new Position(2, pos.y)));
return moves;
}

View File

@ -14,24 +14,33 @@ public class Move {
protected final Position pos, dest;
protected final int xDist, yDist, xSign, ySign;
public Type type;
public Move(Position pos, Position dest, Type type) {
public Move(Position pos, Position dest) {
this.pos = pos;
this.dest = dest;
this.type = type;
xDist = Math.abs(dest.x - pos.x);
yDist = Math.abs(dest.y - pos.y);
xSign = (int) Math.signum(dest.x - pos.x);
ySign = (int) Math.signum(dest.y - pos.y);
}
public Move(Position pos, Position dest) { this(pos, dest, Type.NORMAL); }
public Move(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); }
// TODO: Pawn promotion
public static Move fromLAN(String move) { return new Move(Position.fromLAN(move.substring(0, 2)), Position.fromLAN(move.substring(2))); }
public void execute(Board board) {
// Move the piece to the move's destination square and clean the old position
board.set(dest, board.get(pos));
board.set(pos, null);
}
public void revert(Board board, Piece capturedPiece) {
// Move the piece to the move's position square and clean the destination
board.set(pos, board.get(dest));
board.set(dest, capturedPiece);
}
public String toLAN() { return getPos().toLAN() + getDest().toLAN(); }
public boolean isHorizontal() { return getyDist() == 0; }
@ -44,7 +53,7 @@ public class Move {
public String toString() { return String.format("%s -> %s", getPos(), getDest()); }
@Override
public int hashCode() { return Objects.hash(getDest(), getPos(), getType(), getxDist(), getxSign(), getyDist(), getySign()); }
public int hashCode() { return Objects.hash(getDest(), getPos(), getxDist(), getxSign(), getyDist(), getySign()); }
@Override
public boolean equals(Object obj) {
@ -52,9 +61,8 @@ public class Move {
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Move other = (Move) obj;
return Objects.equals(getDest(), other.getDest()) && Objects.equals(getPos(), other.getPos()) && getType() == other.getType()
&& getxDist() == other.getxDist() && getxSign() == other.getxSign() && getyDist() == other.getyDist()
&& getySign() == other.getySign();
return Objects.equals(getDest(), other.getDest()) && Objects.equals(getPos(), other.getPos()) && getxDist() == other.getxDist()
&& getxSign() == other.getxSign() && getyDist() == other.getyDist() && getySign() == other.getySign();
}
public Position getPos() { return pos; }
@ -68,10 +76,4 @@ public class Move {
public int getxSign() { return xSign; }
public int getySign() { return ySign; }
public Type getType() { return type; }
public static enum Type {
NORMAL, PAWN_PROMOTION, CASTLING, EN_PASSANT, UNKNOWN
}
}

View File

@ -13,31 +13,18 @@ import java.util.List;
*/
public class Pawn extends Piece {
public Pawn(Color color, Board board) {
super(color, board);
}
public Pawn(Color color, Board board) { super(color, board); }
@Override
public boolean isValidMove(Move move) {
boolean step = move.isVertical() && move.getyDist() == 1;
boolean doubleStep = move.isVertical() && move.getyDist() == 2;
boolean strafe = move.isDiagonal() && move.getxDist() == 1;
boolean enPassant = false;
boolean enPassant = strafe && move.getDest().equals(board.getLog().getEnPassant());
if (getColor() == Color.WHITE) doubleStep &= move.getPos().y == 6;
else doubleStep &= move.getPos().y == 1;
// Mark move as pawn promotion if necessary
if (move.getySign() == 1 && move.getPos().y == 6 || move.getySign() == -1 && move.getPos().y == 1)
move.type = Move.Type.PAWN_PROMOTION; // TODO: Remove
// Mark the move as en passant if necessary
if (strafe && move.getDest().equals(board.getLog().getEnPassant())) {
enPassant = true;
move.type = Move.Type.EN_PASSANT;
}
return enPassant || (step ^ doubleStep ^ strafe) && move.getySign() == (getColor() == Color.WHITE ? -1 : 1)
&& isFreePath(move);
return enPassant || (step ^ doubleStep ^ strafe) && move.getySign() == (getColor() == Color.WHITE ? -1 : 1) && isFreePath(move);
}
@Override
@ -53,48 +40,47 @@ public class Pawn extends Piece {
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
int sign = getColor() == Color.WHITE ? -1 : 1;
List<Move> moves = new ArrayList<>();
int sign = getColor() == Color.WHITE ? -1 : 1;
boolean pawnPromotion = sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1;
// Strafe left
if (pos.x > 0) {
Move move = new Move(pos, new Position(pos.x - 1, pos.y + sign));
if (isFreePath(move)) moves.add(move);
}
if (pos.x > 0) addMoveIfValid(moves, pos, new Position(pos.x - 1, pos.y + sign), pawnPromotion);
// Strafe right
if (pos.x < 7) {
Move move = new Move(pos, new Position(pos.x + 1, pos.y + sign));
if (isFreePath(move)) moves.add(move);
}
if (pos.x < 7) addMoveIfValid(moves, pos, new Position(pos.x + 1, pos.y + sign), pawnPromotion);
// Step forward
if (sign == 1 && pos.y < 7 || sign == -1 && pos.y > 0) {
Move move = new Move(pos, new Position(pos.x, pos.y + sign));
if (isFreePath(move)) moves.add(move);
}
if (sign == 1 && pos.y < 7 || sign == -1 && pos.y > 0) addMoveIfValid(moves, pos, new Position(pos.x, pos.y + sign), pawnPromotion);
// Double step forward
if (sign == 1 && pos.y == 1 || sign == -1 && pos.y == 6) {
Move move = new Move(pos, new Position(pos.x, pos.y + 2 * sign));
if (isFreePath(move)) moves.add(move);
}
// TODO: Check against instance
// Mark moves as pawn promotion if necessary
if (sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1)
moves.parallelStream().forEach(m -> m.type = Move.Type.PAWN_PROMOTION);
if (sign == 1 && pos.y == 1 || sign == -1 && pos.y == 6) addMoveIfValid(moves, pos, new Position(pos.x, pos.y + 2 * sign), pawnPromotion);
// Add en passant move if necessary
if (board.getLog().getEnPassant() != null) {
Move move = new Move(pos, board.getLog().getEnPassant(), Move.Type.EN_PASSANT);
Move move = new EnPassant(pos, board.getLog().getEnPassant());
if (move.isDiagonal() && move.getxDist() == 1) moves.add(move);
}
return moves;
}
private void addMoveIfValid(List<Move> moves, Position pos, Position dest, boolean pawnPromotion) {
Move move = new Move(pos, dest);
if (isFreePath(move)) {
if (pawnPromotion) {
try {
moves.add(new PawnPromotion(pos, dest, Queen.class));
moves.add(new PawnPromotion(pos, dest, Rook.class));
moves.add(new PawnPromotion(pos, dest, Knight.class));
moves.add(new PawnPromotion(pos, dest, Bishop.class));
} catch (NoSuchMethodException | SecurityException e) {
e.printStackTrace();
}
} else moves.add(move);
}
}
@Override
public Type getType() { return Type.PAWN; }
}

View File

@ -0,0 +1,46 @@
package dev.kske.chess.board;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import dev.kske.chess.board.Piece.Color;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>PawnPromotion.java</strong><br>
* Created: <strong>2 Nov 2019</strong><br>
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class PawnPromotion extends Move {
private Constructor<? extends Piece> promotionPieceConstructor;
public PawnPromotion(Position pos, Position dest, Class<? extends Piece> promotionPiece) throws NoSuchMethodException, SecurityException {
super(pos, dest);
promotionPieceConstructor = promotionPiece.getDeclaredConstructor(Color.class, Board.class);
promotionPieceConstructor.setAccessible(true);
}
public PawnPromotion(int xPos, int yPos, int xDest, int yDest, Class<? extends Piece> promotionPiece)
throws NoSuchMethodException, SecurityException {
this(new Position(xPos, yPos), new Position(xDest, yDest), promotionPiece);
}
@Override
public void execute(Board board) {
try {
board.set(pos, promotionPieceConstructor.newInstance(board.get(pos).getColor(), board));
super.execute(board);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException e) {
e.printStackTrace();
}
}
@Override
public void revert(Board board, Piece capturedPiece) {
super.revert(board, capturedPiece);
board.set(pos, new Pawn(board.get(dest).getColor(), board));
}
}

View File

@ -7,7 +7,6 @@ import java.util.stream.Collectors;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Move.Type;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Position;
import dev.kske.chess.ui.OverlayComponent;
@ -37,42 +36,32 @@ public class NaturalPlayer extends Player implements MouseListener {
}
@Override
public void requestMove() {
moveRequested = true;
}
public void requestMove() { moveRequested = true; }
@Override
public void cancelMove() {
moveRequested = false;
}
public void cancelMove() { moveRequested = false; }
@Override
public void disconnect() {
overlayComponent.removeMouseListener(this);
}
public void disconnect() { overlayComponent.removeMouseListener(this); }
@Override
public void mousePressed(MouseEvent evt) {
if (!moveRequested) return;
if (pos == null) {
pos = new Position(evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize());
pos = new Position(evt.getPoint().x / overlayComponent.getTileSize(), evt.getPoint().y / overlayComponent.getTileSize());
Board board = new Board(this.board);
if (board.get(pos) != null && board.get(pos).getColor() == color) {
List<Position> positions = board.getMoves(pos)
.stream()
.map(move -> move.getDest())
.collect(Collectors.toList());
List<Position> positions = board.getMoves(pos).stream().map(move -> move.getDest()).collect(Collectors.toList());
overlayComponent.displayDots(positions);
} else pos = null;
} else {
Position dest = new Position(evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize());
Position dest = new Position(evt.getPoint().x / overlayComponent.getTileSize(), evt.getPoint().y / overlayComponent.getTileSize());
overlayComponent.clearDots();
moveRequested = false;
game.onMove(NaturalPlayer.this, new Move(pos, dest, Type.UNKNOWN));
// TODO: Special moves
game.onMove(NaturalPlayer.this, new Move(pos, dest));
pos = null;
}
}