Fixed castling, added castling export to FEN
+ isFreePath implementation in Piece - Removed isFreePath from Bishop, Rook, Queen and King + canCastleKingside and canCastleQueenside methods in King + Castling rights record in Board + FEN export + equals method in Position + UCI 'position startpos' command - Switched to Java 8 compliance for compatibility reasons
This commit is contained in:
parent
ff68def767
commit
91716e12cf
@ -7,7 +7,7 @@
|
|||||||
<attribute name="test" value="true"/>
|
<attribute name="test" value="true"/>
|
||||||
</attributes>
|
</attributes>
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER">
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/jdk1.8.0_212">
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="module" value="true"/>
|
<attribute name="module" value="true"/>
|
||||||
</attributes>
|
</attributes>
|
||||||
|
@ -20,14 +20,6 @@ public class Bishop extends Piece {
|
|||||||
return move.isDiagonal() && isFreePath(move);
|
return move.isDiagonal() && isFreePath(move);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isFreePath(Move move) {
|
|
||||||
for (int i = move.pos.x + move.xSign, j = move.pos.y
|
|
||||||
+ move.ySign; i != move.dest.x; i += move.xSign, j += move.ySign)
|
|
||||||
if (board.getBoardArr()[i][j] != null) return false;
|
|
||||||
return checkDestination(move);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<Move> getPseudolegalMoves(Position pos) {
|
protected List<Move> getPseudolegalMoves(Position pos) {
|
||||||
List<Move> moves = new ArrayList<>();
|
List<Move> moves = new ArrayList<>();
|
||||||
|
@ -19,6 +19,7 @@ public class Board implements Cloneable {
|
|||||||
|
|
||||||
private Piece[][] boardArr;
|
private Piece[][] boardArr;
|
||||||
private Map<Color, Position> kingPos;
|
private Map<Color, Position> kingPos;
|
||||||
|
private Map<Color, Map<Type, Boolean>> castlingRights;
|
||||||
private Color activeColor;
|
private Color activeColor;
|
||||||
private Log log;
|
private Log log;
|
||||||
|
|
||||||
@ -64,6 +65,7 @@ public class Board implements Cloneable {
|
|||||||
public Board() {
|
public Board() {
|
||||||
boardArr = new Piece[8][8];
|
boardArr = new Piece[8][8];
|
||||||
kingPos = new HashMap<>();
|
kingPos = new HashMap<>();
|
||||||
|
castlingRights = new HashMap<>();
|
||||||
log = new Log();
|
log = new Log();
|
||||||
initializeDefaultPositions();
|
initializeDefaultPositions();
|
||||||
}
|
}
|
||||||
@ -140,8 +142,13 @@ public class Board implements Cloneable {
|
|||||||
// Increment move counter
|
// Increment move counter
|
||||||
getDest(move).incMoveCounter();
|
getDest(move).incMoveCounter();
|
||||||
|
|
||||||
// Update the king's position if the moved piece is the king
|
// Update the king's position if the moved piece is the king and castling
|
||||||
if (piece.getType() == Type.KING) kingPos.put(piece.getColor(), move.dest);
|
// availability
|
||||||
|
if (piece.getType() == Type.KING) {
|
||||||
|
kingPos.put(piece.getColor(), move.dest);
|
||||||
|
castlingRights.get(piece.getColor()).put(Type.KING, false);
|
||||||
|
castlingRights.get(piece.getColor()).put(Type.QUEEN, false);
|
||||||
|
}
|
||||||
|
|
||||||
// Update log
|
// Update log
|
||||||
log.add(move, capturePiece);
|
log.add(move, capturePiece);
|
||||||
@ -154,6 +161,8 @@ public class Board implements Cloneable {
|
|||||||
|
|
||||||
// Increment halfmove clock
|
// Increment halfmove clock
|
||||||
++halfmoveClock;
|
++halfmoveClock;
|
||||||
|
|
||||||
|
updateCastlingRights();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -211,6 +220,30 @@ public class Board implements Cloneable {
|
|||||||
|
|
||||||
// Decrement halfmove clock
|
// Decrement halfmove clock
|
||||||
--halfmoveClock;
|
--halfmoveClock;
|
||||||
|
|
||||||
|
updateCastlingRights();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCastlingRights() {
|
||||||
|
// White
|
||||||
|
if (new Position(4, 7).equals(kingPos.get(Color.WHITE))) {
|
||||||
|
final King king = (King) get(kingPos.get(Color.WHITE));
|
||||||
|
castlingRights.get(Color.WHITE).put(Type.KING, king.canCastleKingside());
|
||||||
|
castlingRights.get(Color.WHITE).put(Type.QUEEN, king.canCastleQueenside());
|
||||||
|
} else {
|
||||||
|
castlingRights.get(Color.WHITE).put(Type.KING, false);
|
||||||
|
castlingRights.get(Color.WHITE).put(Type.QUEEN, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Black
|
||||||
|
if (new Position(4, 0).equals(kingPos.get(Color.BLACK))) {
|
||||||
|
final King king = (King) get(kingPos.get(Color.BLACK));
|
||||||
|
castlingRights.get(Color.BLACK).put(Type.KING, king.canCastleKingside());
|
||||||
|
castlingRights.get(Color.BLACK).put(Type.QUEEN, king.canCastleQueenside());
|
||||||
|
} else {
|
||||||
|
castlingRights.get(Color.BLACK).put(Type.KING, false);
|
||||||
|
castlingRights.get(Color.BLACK).put(Type.QUEEN, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -354,6 +387,16 @@ public class Board implements Cloneable {
|
|||||||
for (int j = 2; j < 6; j++)
|
for (int j = 2; j < 6; j++)
|
||||||
boardArr[i][j] = null;
|
boardArr[i][j] = null;
|
||||||
|
|
||||||
|
// Initialize castling rights
|
||||||
|
castlingRights.clear();
|
||||||
|
Map<Type, Boolean> whiteCastling = new HashMap<>(), blackCastling = new HashMap<>();
|
||||||
|
whiteCastling.put(Type.KING, true);
|
||||||
|
whiteCastling.put(Type.QUEEN, true);
|
||||||
|
blackCastling.put(Type.KING, true);
|
||||||
|
blackCastling.put(Type.QUEEN, true);
|
||||||
|
castlingRights.put(Color.WHITE, whiteCastling);
|
||||||
|
castlingRights.put(Color.BLACK, blackCastling);
|
||||||
|
|
||||||
activeColor = Color.WHITE;
|
activeColor = Color.WHITE;
|
||||||
|
|
||||||
fullmoveCounter = 1;
|
fullmoveCounter = 1;
|
||||||
@ -437,8 +480,14 @@ public class Board implements Cloneable {
|
|||||||
// Active color
|
// Active color
|
||||||
sb.append(" " + (activeColor == Color.WHITE ? 'w' : 'b'));
|
sb.append(" " + (activeColor == Color.WHITE ? 'w' : 'b'));
|
||||||
|
|
||||||
// TODO: castling rights
|
sb.append(' ');
|
||||||
sb.append(" -");
|
StringBuilder castlingSb = new StringBuilder();
|
||||||
|
if (castlingRights.get(Color.WHITE).get(Type.KING)) castlingSb.append('K');
|
||||||
|
if (castlingRights.get(Color.WHITE).get(Type.QUEEN)) castlingSb.append('Q');
|
||||||
|
if (castlingRights.get(Color.BLACK).get(Type.KING)) castlingSb.append('k');
|
||||||
|
if (castlingRights.get(Color.BLACK).get(Type.QUEEN)) castlingSb.append('q');
|
||||||
|
if (castlingSb.length() == 0) sb.append("-");
|
||||||
|
sb.append(castlingSb);
|
||||||
|
|
||||||
// TODO: en passant availability
|
// TODO: en passant availability
|
||||||
sb.append(" -");
|
sb.append(" -");
|
||||||
@ -481,5 +530,8 @@ public class Board implements Cloneable {
|
|||||||
*/
|
*/
|
||||||
public Piece[][] getBoardArr() { return boardArr; }
|
public Piece[][] getBoardArr() { return boardArr; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The active color for the next move
|
||||||
|
*/
|
||||||
public Color getActiveColor() { return activeColor; }
|
public Color getActiveColor() { return activeColor; }
|
||||||
}
|
}
|
||||||
|
@ -18,27 +18,40 @@ public class King extends Piece {
|
|||||||
@Override
|
@Override
|
||||||
public boolean isValidMove(Move move) {
|
public boolean isValidMove(Move move) {
|
||||||
// Castling
|
// Castling
|
||||||
if (getMoveCounter() == 0 && move.xDist == 2 && move.yDist == 0) {
|
if (move.xDist == 2 && move.yDist == 0) {
|
||||||
|
if (canCastleKingside()) {
|
||||||
// Kingside
|
|
||||||
if (board.getBoardArr()[7][move.pos.y] != null && board.getBoardArr()[7][move.pos.y].getType() == Type.ROOK
|
|
||||||
&& isFreePath(new Move(new Position(5, move.pos.y), new Position(7, move.pos.y)))) {
|
|
||||||
move.type = Move.Type.CASTLING;
|
move.type = Move.Type.CASTLING;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (canCastleQueenside()) {
|
||||||
// Queenside
|
|
||||||
if (board.getBoardArr()[0][move.pos.y] != null && board.getBoardArr()[0][move.pos.y].getType() == Type.ROOK
|
|
||||||
&& isFreePath(new Move(new Position(1, move.pos.y), new Position(4, move.pos.y)))) {
|
|
||||||
move.type = Move.Type.CASTLING;
|
move.type = Move.Type.CASTLING;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return move.xDist <= 1 && move.yDist <= 1 && checkDestination(move);
|
return move.xDist <= 1 && move.yDist <= 1 && checkDestination(move);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean canCastleKingside() {
|
||||||
|
if (getMoveCounter() == 0) {
|
||||||
|
int y = getColor() == Color.WHITE ? 7 : 0;
|
||||||
|
Position rookPos = new Position(7, y);
|
||||||
|
Piece rook = board.get(rookPos);
|
||||||
|
return rook != null && rook.getType() == Type.ROOK && rook.getMoveCounter() == 0
|
||||||
|
&& isFreePath(new Move(new Position(4, y), new Position(6, y)));
|
||||||
|
} else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canCastleQueenside() {
|
||||||
|
if (getMoveCounter() == 0) {
|
||||||
|
int y = getColor() == Color.WHITE ? 7 : 0;
|
||||||
|
Position rookPos = new Position(0, y);
|
||||||
|
Piece rook = board.get(rookPos);
|
||||||
|
return rook != null && rook.getType() == Type.ROOK && rook.getMoveCounter() == 0
|
||||||
|
&& isFreePath(new Move(new Position(4, y), new Position(1, y)));
|
||||||
|
} else return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<Move> getPseudolegalMoves(Position pos) {
|
protected List<Move> getPseudolegalMoves(Position pos) {
|
||||||
List<Move> moves = new ArrayList<>();
|
List<Move> moves = new ArrayList<>();
|
||||||
@ -50,31 +63,13 @@ public class King extends Piece {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Castling
|
// Castling
|
||||||
// TODO: Check attacked squares in between
|
// TODO: Condition: cannot castle out of, through or into check
|
||||||
// TODO: Castling out of check?
|
if (canCastleKingside()) moves.add(new Move(pos, new Position(6, pos.y), Move.Type.CASTLING));
|
||||||
if (getMoveCounter() == 0) {
|
if (canCastleQueenside()) moves.add(new Move(pos, new Position(2, pos.y), Move.Type.CASTLING));
|
||||||
|
|
||||||
// Kingside
|
|
||||||
if (board.getBoardArr()[7][pos.y] != null && board.getBoardArr()[7][pos.y].getType() == Type.ROOK
|
|
||||||
&& isFreePath(new Move(new Position(5, pos.y), new Position(7, pos.y))))
|
|
||||||
moves.add(new Move(pos, new Position(6, pos.y), Move.Type.CASTLING));
|
|
||||||
|
|
||||||
// Queenside
|
|
||||||
if (board.getBoardArr()[0][pos.y] != null && board.getBoardArr()[0][pos.y].getType() == Type.ROOK
|
|
||||||
&& isFreePath(new Move(new Position(1, pos.y), new Position(4, pos.y))))
|
|
||||||
moves.add(new Move(pos, new Position(2, pos.y), Move.Type.CASTLING));
|
|
||||||
}
|
|
||||||
|
|
||||||
return moves;
|
return moves;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isFreePath(Move move) {
|
|
||||||
for (int i = move.pos.x, j = move.pos.y; i != move.dest.x || j != move.dest.y; i += move.xSign, j += move.ySign)
|
|
||||||
if (board.getBoardArr()[i][j] != null) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Type getType() { return Type.KING; }
|
public Type getType() { return Type.KING; }
|
||||||
}
|
}
|
||||||
|
@ -35,8 +35,16 @@ public abstract class Piece implements Cloneable {
|
|||||||
|
|
||||||
public abstract boolean isValidMove(Move move);
|
public abstract boolean isValidMove(Move move);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks, if the squares between the position and the destination of a move are
|
||||||
|
* free.
|
||||||
|
*
|
||||||
|
* @param move The move to check
|
||||||
|
*/
|
||||||
protected boolean isFreePath(Move move) {
|
protected boolean isFreePath(Move move) {
|
||||||
// Only check destination by default
|
for (int i = move.pos.x + move.xSign, j = move.pos.y + move.ySign; i != move.dest.x
|
||||||
|
|| j != move.dest.y; i += move.xSign, j += move.ySign)
|
||||||
|
if (board.getBoardArr()[i][j] != null) return false;
|
||||||
return checkDestination(move);
|
return checkDestination(move);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,4 +23,24 @@ public class Position {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("[%d, %d]", x, y);
|
return String.format("[%d, %d]", x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + x;
|
||||||
|
result = prime * result + y;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) return true;
|
||||||
|
if (obj == null) return false;
|
||||||
|
if (getClass() != obj.getClass()) return false;
|
||||||
|
Position other = (Position) obj;
|
||||||
|
if (x != other.x) return false;
|
||||||
|
if (y != other.y) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,22 +20,6 @@ public class Queen extends Piece {
|
|||||||
return ((move.isHorizontal() || move.isVertical()) || move.isDiagonal()) && isFreePath(move);
|
return ((move.isHorizontal() || move.isVertical()) || move.isDiagonal()) && isFreePath(move);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isFreePath(Move move) {
|
|
||||||
if (move.isHorizontal()) {
|
|
||||||
for (int i = move.pos.x + move.xSign; i != move.dest.x; i += move.xSign)
|
|
||||||
if (board.getBoardArr()[i][move.pos.y] != null) return false;
|
|
||||||
} else if (move.isVertical()) {
|
|
||||||
for (int i = move.pos.y + move.ySign; i != move.dest.y; i += move.ySign)
|
|
||||||
if (board.getBoardArr()[move.pos.x][i] != null) return false;
|
|
||||||
} else {
|
|
||||||
for (int i = move.pos.x + move.xSign, j = move.pos.y
|
|
||||||
+ move.ySign; i != move.dest.x; i += move.xSign, j += move.ySign)
|
|
||||||
if (board.getBoardArr()[i][j] != null) return false;
|
|
||||||
}
|
|
||||||
return checkDestination(move);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<Move> getPseudolegalMoves(Position pos) {
|
protected List<Move> getPseudolegalMoves(Position pos) {
|
||||||
List<Move> moves = new ArrayList<>();
|
List<Move> moves = new ArrayList<>();
|
||||||
|
@ -20,18 +20,6 @@ public class Rook extends Piece {
|
|||||||
return (move.isHorizontal() || move.isVertical()) && isFreePath(move);
|
return (move.isHorizontal() || move.isVertical()) && isFreePath(move);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isFreePath(Move move) {
|
|
||||||
if (move.isHorizontal()) {
|
|
||||||
for (int i = move.pos.x + move.xSign; i != move.dest.x; i += move.xSign)
|
|
||||||
if (board.getBoardArr()[i][move.pos.y] != null) return false;
|
|
||||||
} else {
|
|
||||||
for (int i = move.pos.y + move.ySign; i != move.dest.y; i += move.ySign)
|
|
||||||
if (board.getBoardArr()[move.pos.x][i] != null) return false;
|
|
||||||
}
|
|
||||||
return checkDestination(move);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<Move> getPseudolegalMoves(Position pos) {
|
protected List<Move> getPseudolegalMoves(Position pos) {
|
||||||
List<Move> moves = new ArrayList<>();
|
List<Move> moves = new ArrayList<>();
|
||||||
|
@ -94,6 +94,13 @@ public class UCIHandle {
|
|||||||
|
|
||||||
// TODO: position
|
// TODO: position
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the position in its initial state.
|
||||||
|
*/
|
||||||
|
public void startPosition() {
|
||||||
|
out.println("position startpos");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the position described in the FEN string.
|
* Sets up the position described in the FEN string.
|
||||||
*
|
*
|
||||||
|
@ -32,7 +32,7 @@ public class UCIReceiver implements Runnable {
|
|||||||
String line;
|
String line;
|
||||||
while (!Thread.currentThread().isInterrupted())
|
while (!Thread.currentThread().isInterrupted())
|
||||||
try {
|
try {
|
||||||
if ((line = in.readLine()) != null && !line.isBlank()) parse(line);
|
if ((line = in.readLine()) != null && !line.isEmpty()) parse(line);
|
||||||
} catch (IndexOutOfBoundsException ex) {
|
} catch (IndexOutOfBoundsException ex) {
|
||||||
System.err.println("Too few arguments were provided!");
|
System.err.println("Too few arguments were provided!");
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
|
54
src/dev/kske/chess/ui/LogFrame.java
Normal file
54
src/dev/kske/chess/ui/LogFrame.java
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package dev.kske.chess.ui;
|
||||||
|
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
|
||||||
|
import javax.swing.JFrame;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTable;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import javax.swing.table.DefaultTableModel;
|
||||||
|
|
||||||
|
import dev.kske.chess.board.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project: <strong>Chess</strong><br>
|
||||||
|
* File: <strong>LogFrame.java</strong><br>
|
||||||
|
* Created: <strong>17.07.2019</strong><br>
|
||||||
|
* Author: <strong>Kai S. K. Engelbart</strong>
|
||||||
|
*/
|
||||||
|
public class LogFrame extends JFrame {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1932671698254197119L;
|
||||||
|
|
||||||
|
private JPanel mcontentPane;
|
||||||
|
private JTable mtable;
|
||||||
|
|
||||||
|
private Log log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the frame.
|
||||||
|
*/
|
||||||
|
public LogFrame() {
|
||||||
|
setTitle("Move History");
|
||||||
|
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
|
setBounds(100, 100, 450, 300);
|
||||||
|
mcontentPane = new JPanel();
|
||||||
|
mcontentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
|
||||||
|
mcontentPane.setLayout(new BorderLayout(0, 0));
|
||||||
|
setContentPane(mcontentPane);
|
||||||
|
|
||||||
|
mtable = new JTable();
|
||||||
|
mtable.setModel(new DefaultTableModel(new Object[][] {}, new String[] { "Black", "New column" }));
|
||||||
|
mtable.setEnabled(false);
|
||||||
|
mcontentPane.add(mtable, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Log getLog() { return log; }
|
||||||
|
|
||||||
|
public void setLog(Log log) { this.log = log; }
|
||||||
|
|
||||||
|
}
|
@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeEach;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import dev.kske.chess.board.Board;
|
import dev.kske.chess.board.Board;
|
||||||
|
import dev.kske.chess.board.Move;
|
||||||
import dev.kske.chess.board.Piece.Color;
|
import dev.kske.chess.board.Piece.Color;
|
||||||
import dev.kske.chess.board.Queen;
|
import dev.kske.chess.board.Queen;
|
||||||
|
|
||||||
@ -38,6 +39,8 @@ class BoardTest {
|
|||||||
assertNotSame(clone.getBoardArr(), board.getBoardArr());
|
assertNotSame(clone.getBoardArr(), board.getBoardArr());
|
||||||
|
|
||||||
clone.getBoardArr()[0][0] = new Queen(Color.BLACK, clone);
|
clone.getBoardArr()[0][0] = new Queen(Color.BLACK, clone);
|
||||||
|
clone.move(new Move(1, 1, 1, 2));
|
||||||
assertNotEquals(clone.getBoardArr()[0][0], board.getBoardArr()[0][0]);
|
assertNotEquals(clone.getBoardArr()[0][0], board.getBoardArr()[0][0]);
|
||||||
|
assertNotEquals(clone.getActiveColor(), board.getActiveColor());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user