From d7479f4095e29063a77fad707e06d14d08bd9c6e Mon Sep 17 00:00:00 2001 From: ammaster10s Date: Sun, 27 Sep 2026 14:07:47 +0700 Subject: [PATCH] Rebuild chess game with tested rules and Swing UI --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 4 + .vscode/settings.json | 3 - ChessApp.java | 940 ++++++++++++++++++++++++++++++++++++++++++ ChessGame.java | 432 +++++++++++++++++++ ChessGameTest.java | 267 ++++++++++++ Chessboard.java | 213 ---------- Chessclock.java | 186 --------- ImageCreater.java | 143 ------- MouseMotion.java | 179 -------- Piece.java | 451 -------------------- README.md | 40 ++ TestGraphics.java | 56 --- build.sh | 9 + docs/preview.png | Bin 0 -> 76682 bytes run.sh | 6 + test.sh | 8 + 17 files changed, 1706 insertions(+), 1231 deletions(-) delete mode 100644 .DS_Store create mode 100644 .gitignore delete mode 100644 .vscode/settings.json create mode 100644 ChessApp.java create mode 100644 ChessGame.java create mode 100644 ChessGameTest.java delete mode 100644 Chessboard.java delete mode 100644 Chessclock.java delete mode 100644 ImageCreater.java delete mode 100644 MouseMotion.java delete mode 100644 Piece.java create mode 100644 README.md delete mode 100644 TestGraphics.java create mode 100755 build.sh create mode 100644 docs/preview.png create mode 100755 run.sh create mode 100755 test.sh diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 552dba095730b87b9f456041b8db7d86e2238d9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJx{|h6g)!*Xa@=t3sPnVB>q8YL5Kkf2_#n95~V?PT z3IqlInga6ew$MQzcbKC1{ibK5Y|tNNeYTM0;fU$rON}L9ldK1_#z@^4UuJxi7)>(9 zWTf;ntLChwTuSy!UY^jyE17m(DS0t_6<(e(N5ZR)^H$=>Tqaz*GR~;l%V&wPg)V22 zP_rqu%Q(UbZppo8=>YZX$l1mfTTeOCQ;xL9Jz~f{ z2CS)3&4}j39$L6w9jB^wk+n(I@;GH=dokA}V?~_G*gTGSSsJJE4m^f2CLOrmY)UTL=Izz zxkWyq8A~Nvs&PjQW9h7qT)Ws|Zqd?V+~LDG&&HiljCyDPkv4~kErvWO5ESqgShu%r z+5b=K&;LFt+yw=K0{=<@(>T67?r=(OZ(W$2?6nco4U?F(%PlH}ncI%FAY1V+lNz5z XQXs|-bBnB@g&zTxA!I>;Usd26q&^Xc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89a4565 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +*.class +build/ +dist/ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c995aa5..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.debug.settings.onBuildFailureProceed": true -} \ No newline at end of file diff --git a/ChessApp.java b/ChessApp.java new file mode 100644 index 0000000..9892af6 --- /dev/null +++ b/ChessApp.java @@ -0,0 +1,940 @@ +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JToggleButton; +import javax.swing.ListSelectionModel; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.UIManager; +import javax.swing.WindowConstants; +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GridLayout; +import java.awt.RenderingHints; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** A local two-player Swing chess game. ChessGame owns the rules and position. */ +public final class ChessApp { + private static final Color BACKGROUND = new Color(22, 32, 38); + private static final Color PANEL = new Color(29, 43, 49); + private static final Color PANEL_RAISED = new Color(41, 58, 64); + private static final Color TEXT = new Color(244, 241, 231); + private static final Color MUTED = new Color(187, 203, 201); + private static final Color ACCENT = new Color(225, 183, 105); + private static final Color DANGER = new Color(234, 139, 126); + private static final Color LIGHT_SQUARE = new Color(234, 230, 214); + private static final Color DARK_SQUARE = new Color(106, 139, 127); + + private final ChessGame game = new ChessGame(); + private final Map> images = + new EnumMap<>(ChessGame.Side.class); + private final BoardPanel board = new BoardPanel(); + private final ClockCard blackClock = new ClockCard("BLACK"); + private final ClockCard whiteClock = new ClockCard("WHITE"); + private final JLabel status = new JLabel(); + private final JLabel hint = new JLabel(); + private final DefaultListModel moveModel = new DefaultListModel<>(); + private final JList moveList = new JList<>(moveModel); + private final JPanel moveContent = new JPanel(new BorderLayout()); + private final List timeButtons = new ArrayList<>(); + private final Timer clockTimer = new Timer(100, this::onClockTick); + private PaintButton drawButton; + private PaintButton resignButton; + + private JFrame frame; + private TimeControl timeControl = TimeControl.FIVE_FIVE; + private ChessGame.Result engineResult = ChessGame.Result.ONGOING; + private String localResult; + private List selectedMoves = List.of(); + private ChessGame.Move lastMove; + private int selectedRow = -1; + private int selectedCol = -1; + private int ply; + private boolean clockStarted; + private long lastTickNanos; + private long whiteMillis; + private long blackMillis; + + private enum TimeControl { + THREE_TWO("3 + 2", 3, 2), + FIVE_FIVE("5 + 5", 5, 5), + TEN_ZERO("10 + 0", 10, 0); + + final String label; + final int minutes; + final int incrementSeconds; + + TimeControl(String label, int minutes, int incrementSeconds) { + this.label = label; + this.minutes = minutes; + this.incrementSeconds = incrementSeconds; + } + } + + private ChessApp() { + loadImages(); + resetGame(); + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception ignored) { + // The default Swing look and feel remains usable. + } + new ChessApp().show(); + }); + } + + private void show() { + frame = new JFrame("Chess — Local match"); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + frame.setMinimumSize(new Dimension(790, 590)); + + JPanel root = new JPanel(new BorderLayout()); + root.setBackground(BACKGROUND); + root.add(createHeader(), BorderLayout.NORTH); + + JPanel playArea = new JPanel(new BorderLayout(14, 0)); + playArea.setBackground(BACKGROUND); + playArea.setBorder(BorderFactory.createEmptyBorder(0, 14, 14, 14)); + playArea.add(board, BorderLayout.CENTER); + playArea.add(createSidebar(), BorderLayout.EAST); + root.add(playArea, BorderLayout.CENTER); + + frame.setContentPane(root); + frame.setSize(1120, 760); + frame.setLocationRelativeTo(null); + refreshStatus(); + refreshClocks(); + frame.setVisible(true); + } + + private JPanel createHeader() { + JPanel header = new JPanel(new BorderLayout()); + header.setBackground(BACKGROUND); + header.setBorder(BorderFactory.createEmptyBorder(15, 23, 14, 25)); + + JPanel heading = new JPanel(); + heading.setOpaque(false); + heading.setLayout(new BoxLayout(heading, BoxLayout.Y_AXIS)); + JLabel title = new JLabel("Chess"); + title.setForeground(TEXT); + title.setFont(new Font("SansSerif", Font.BOLD, 25)); + JLabel subtitle = new JLabel("Local two-player match"); + subtitle.setForeground(MUTED); + subtitle.setFont(new Font("SansSerif", Font.PLAIN, 12)); + heading.add(title); + heading.add(subtitle); + header.add(heading, BorderLayout.WEST); + + PaintButton newGame = new PaintButton("New game", PaintButton.Style.PRIMARY); + newGame.addActionListener(e -> resetGame()); + header.add(newGame, BorderLayout.EAST); + return header; + } + + private JPanel createSidebar() { + JPanel sidebar = new JPanel(new BorderLayout(0, 18)); + sidebar.setPreferredSize(new Dimension(288, 640)); + sidebar.setBackground(PANEL); + sidebar.setBorder(BorderFactory.createEmptyBorder(17, 17, 17, 17)); + + JPanel top = new JPanel(); + top.setOpaque(false); + top.setLayout(new BoxLayout(top, BoxLayout.Y_AXIS)); + blackClock.setAlignmentX(0); + whiteClock.setAlignmentX(0); + top.add(blackClock); + top.add(Box.createVerticalStrut(9)); + top.add(whiteClock); + top.add(Box.createVerticalStrut(18)); + + JPanel statusPanel = new JPanel(); + statusPanel.setOpaque(false); + statusPanel.setLayout(new BoxLayout(statusPanel, BoxLayout.Y_AXIS)); + statusPanel.setAlignmentX(0); + status.setFont(new Font("SansSerif", Font.BOLD, 17)); + status.setForeground(TEXT); + status.setAlignmentX(0); + hint.setFont(new Font("SansSerif", Font.PLAIN, 12)); + hint.setForeground(MUTED); + hint.setAlignmentX(0); + statusPanel.add(status); + statusPanel.add(Box.createVerticalStrut(5)); + statusPanel.add(hint); + top.add(statusPanel); + top.add(Box.createVerticalStrut(21)); + + JLabel timeLabel = sectionLabel("Time control"); + timeLabel.setAlignmentX(0); + top.add(timeLabel); + top.add(Box.createVerticalStrut(9)); + JPanel presets = new JPanel(new GridLayout(1, 3, 7, 0)); + presets.setOpaque(false); + presets.setAlignmentX(0); + presets.setMaximumSize(new Dimension(Integer.MAX_VALUE, 35)); + ButtonGroup group = new ButtonGroup(); + for (TimeControl control : TimeControl.values()) { + TimeButton button = new TimeButton(control); + button.setSelected(control == timeControl); + button.addActionListener(e -> chooseTimeControl(control)); + group.add(button); + timeButtons.add(button); + presets.add(button); + } + top.add(presets); + sidebar.add(top, BorderLayout.NORTH); + + JPanel history = new JPanel(new BorderLayout(0, 10)); + history.setOpaque(false); + history.add(sectionLabel("Moves"), BorderLayout.NORTH); + moveContent.setOpaque(false); + moveList.setBackground(PANEL_RAISED); + moveList.setForeground(TEXT); + moveList.setFont(new Font("Monospaced", Font.PLAIN, 13)); + moveList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + moveList.setFocusable(false); + moveList.setCellRenderer(new MoveRenderer()); + JScrollPane scroller = new JScrollPane(moveList); + scroller.setBorder(BorderFactory.createEmptyBorder()); + scroller.getViewport().setBackground(PANEL_RAISED); + scroller.getVerticalScrollBar().setUnitIncrement(18); + moveContent.add(scroller, BorderLayout.CENTER); + history.add(moveContent, BorderLayout.CENTER); + sidebar.add(history, BorderLayout.CENTER); + + JPanel actions = new JPanel(new GridLayout(1, 2, 9, 0)); + actions.setOpaque(false); + drawButton = new PaintButton("Agree draw", PaintButton.Style.SECONDARY); + drawButton.addActionListener(e -> agreeDraw()); + resignButton = new PaintButton("Resign", PaintButton.Style.DANGER); + resignButton.addActionListener(e -> resign()); + actions.add(drawButton); + actions.add(resignButton); + sidebar.add(actions, BorderLayout.SOUTH); + updateMoveContent(); + return sidebar; + } + + private static JLabel sectionLabel(String text) { + JLabel label = new JLabel(text); + label.setForeground(MUTED); + label.setFont(new Font("SansSerif", Font.BOLD, 12)); + return label; + } + + private void chooseTimeControl(TimeControl control) { + if (clockStarted || isFinished()) return; + timeControl = control; + whiteMillis = blackMillis = control.minutes * 60_000L; + refreshClocks(); + } + + private void resetGame() { + clockTimer.stop(); + game.reset(); + engineResult = ChessGame.Result.ONGOING; + localResult = null; + selectedMoves = List.of(); + selectedRow = selectedCol = -1; + lastMove = null; + ply = 0; + clockStarted = false; + whiteMillis = blackMillis = timeControl.minutes * 60_000L; + moveModel.clear(); + updateMoveContent(); + for (TimeButton button : timeButtons) button.setEnabled(true); + refreshStatus(); + refreshClocks(); + board.repaint(); + } + + private void selectSquare(int row, int col) { + if (isFinished()) return; + settleClock(); + if (isFinished()) return; + + if (selectedRow == row && selectedCol == col) { + clearSelection(); + return; + } + ChessGame.Piece piece = game.pieceAt(row, col); + if (piece != null && piece.side == game.sideToMove()) { + selectedRow = row; + selectedCol = col; + selectedMoves = game.legalMovesFrom(row, col); + board.repaint(); + return; + } + List candidates = new ArrayList<>(); + for (ChessGame.Move move : selectedMoves) { + if (move.toRow == row && move.toCol == col) candidates.add(move); + } + if (!candidates.isEmpty()) { + ChessGame.Move chosen = chooseMove(candidates); + if (chosen != null) playMove(chosen); + } else { + clearSelection(); + } + } + + private ChessGame.Move chooseMove(List candidates) { + if (candidates.size() == 1) return candidates.get(0); + ChessGame.PieceType[] order = { + ChessGame.PieceType.QUEEN, ChessGame.PieceType.ROOK, + ChessGame.PieceType.BISHOP, ChessGame.PieceType.KNIGHT + }; + String[] labels = {"Queen", "Rook", "Bishop", "Knight"}; + int choice = JOptionPane.showOptionDialog(frame, "Choose a piece for your pawn.", + "Promote pawn", JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, + null, labels, labels[0]); + if (choice < 0) return null; + for (ChessGame.Move move : candidates) { + if (move.promotion == order[choice]) return move; + } + return null; + } + + private void playMove(ChessGame.Move move) { + // A promotion dialog can stay open while the opponent's clock expires. + if (isFinished()) return; + settleClock(); + if (isFinished()) return; + ChessGame.Side mover = game.sideToMove(); + ChessGame.Piece piece = game.pieceAt(move.fromRow, move.fromCol); + ChessGame.Piece target = game.pieceAt(move.toRow, move.toCol); + boolean capture = target != null || (piece.type == ChessGame.PieceType.PAWN + && move.fromCol != move.toCol); + String notation = moveNotation(move, piece, capture); + if (!game.makeMove(move)) return; + + if (clockStarted) addIncrement(mover); + else { + clockStarted = true; + addIncrement(mover); + for (TimeButton button : timeButtons) button.setEnabled(false); + } + lastTickNanos = System.nanoTime(); + engineResult = game.result(); + if (engineResult == ChessGame.Result.CHECKMATE) notation += "#"; + else if (engineResult == ChessGame.Result.ONGOING && game.isInCheck(game.sideToMove())) { + notation += "+"; + } + addMoveToHistory(mover, notation); + lastMove = move; + selectedMoves = List.of(); + selectedRow = selectedCol = -1; + if (engineResult == ChessGame.Result.ONGOING) clockTimer.start(); + else clockTimer.stop(); + refreshStatus(); + refreshClocks(); + board.repaint(); + } + + private void addMoveToHistory(ChessGame.Side mover, String notation) { + if (mover == ChessGame.Side.WHITE) { + moveModel.addElement(String.format("%2d. %s", ply / 2 + 1, notation)); + } else { + int last = moveModel.size() - 1; + if (last >= 0) moveModel.set(last, moveModel.get(last) + " " + notation); + else moveModel.addElement(String.format("%2d... %s", ply / 2 + 1, notation)); + } + ply++; + updateMoveContent(); + moveList.ensureIndexIsVisible(moveModel.size() - 1); + } + + private void updateMoveContent() { + if (moveContent == null) return; + if (moveModel.isEmpty()) { + moveContent.removeAll(); + JLabel empty = new JLabel("Select a piece to begin", SwingConstants.CENTER); + empty.setForeground(MUTED); + empty.setFont(new Font("SansSerif", Font.PLAIN, 12)); + empty.setOpaque(true); + empty.setBackground(PANEL_RAISED); + moveContent.add(empty, BorderLayout.CENTER); + } else if (!(moveContent.getComponent(0) instanceof JScrollPane)) { + moveContent.removeAll(); + JScrollPane scroller = new JScrollPane(moveList); + scroller.setBorder(BorderFactory.createEmptyBorder()); + scroller.getViewport().setBackground(PANEL_RAISED); + scroller.getVerticalScrollBar().setUnitIncrement(18); + moveContent.add(scroller, BorderLayout.CENTER); + } + moveContent.revalidate(); + moveContent.repaint(); + } + + private static String moveNotation(ChessGame.Move move, ChessGame.Piece piece, + boolean capture) { + if (piece.type == ChessGame.PieceType.KING + && Math.abs(move.toCol - move.fromCol) == 2) { + return move.toCol > move.fromCol ? "O-O" : "O-O-O"; + } + String from = squareName(move.fromRow, move.fromCol); + String to = squareName(move.toRow, move.toCol); + String result = from + (capture ? "×" : "–") + to; + if (move.promotion != null) result += "=" + move.promotion.name().charAt(0); + return result; + } + + private static String squareName(int row, int col) { + return "" + (char) ('a' + col) + (8 - row); + } + + private void clearSelection() { + selectedRow = selectedCol = -1; + selectedMoves = List.of(); + board.repaint(); + } + + private void addIncrement(ChessGame.Side mover) { + long increment = timeControl.incrementSeconds * 1_000L; + if (mover == ChessGame.Side.WHITE) whiteMillis += increment; + else blackMillis += increment; + } + + private void onClockTick(ActionEvent ignored) { + settleClock(); + refreshClocks(); + } + + private void settleClock() { + if (!clockStarted || isFinished()) return; + long now = System.nanoTime(); + long elapsed = Math.max(0, (now - lastTickNanos) / 1_000_000L); + lastTickNanos += elapsed * 1_000_000L; + if (game.sideToMove() == ChessGame.Side.WHITE) { + whiteMillis = Math.max(0, whiteMillis - elapsed); + if (whiteMillis == 0) finishLocally("Black wins on time"); + } else { + blackMillis = Math.max(0, blackMillis - elapsed); + if (blackMillis == 0) finishLocally("White wins on time"); + } + } + + private void agreeDraw() { + if (isFinished()) return; + int answer = JOptionPane.showConfirmDialog(frame, "End this game as a draw?", + "Agree draw", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + if (answer == JOptionPane.YES_OPTION && !isFinished()) finishLocally("Draw agreed"); + } + + private void resign() { + if (isFinished()) return; + String player = sideName(game.sideToMove()); + String winner = sideName(opposite(game.sideToMove())); + int answer = JOptionPane.showConfirmDialog(frame, + player + " resigns. " + winner + " wins. Continue?", + "Resign game", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (answer == JOptionPane.YES_OPTION && !isFinished()) { + finishLocally(winner + " wins by resignation"); + } + } + + private void finishLocally(String result) { + localResult = result; + clockTimer.stop(); + clearSelection(); + refreshStatus(); + refreshClocks(); + } + + private boolean isFinished() { + return localResult != null || engineResult != ChessGame.Result.ONGOING; + } + + private void refreshStatus() { + if (localResult != null) { + status.setText(localResult); + hint.setText("Start a new game to play again."); + } else if (engineResult == ChessGame.Result.CHECKMATE) { + status.setText(sideName(opposite(game.sideToMove())) + " wins by checkmate"); + hint.setText("Start a new game to play again."); + } else if (engineResult == ChessGame.Result.STALEMATE) { + status.setText("Draw by stalemate"); + hint.setText("Neither player has a legal move."); + } else if (engineResult == ChessGame.Result.DRAW_INSUFFICIENT_MATERIAL) { + status.setText("Draw — insufficient material"); + hint.setText("Neither side can force checkmate."); + } else if (game.isInCheck(game.sideToMove())) { + status.setText(sideName(game.sideToMove()) + " is in check"); + hint.setText("Protect your king with a legal move."); + } else { + status.setText(sideName(game.sideToMove()) + " to move"); + hint.setText(clockStarted ? "Select a piece to see legal moves." + : "The clock starts after the first move."); + } + status.setForeground(game.isInCheck(game.sideToMove()) && !isFinished() ? DANGER : TEXT); + if (drawButton != null) drawButton.setEnabled(!isFinished()); + if (resignButton != null) resignButton.setEnabled(!isFinished()); + } + + private void refreshClocks() { + blackClock.update(blackMillis, game.sideToMove() == ChessGame.Side.BLACK && !isFinished()); + whiteClock.update(whiteMillis, game.sideToMove() == ChessGame.Side.WHITE && !isFinished()); + } + + private static String sideName(ChessGame.Side side) { + return side == ChessGame.Side.WHITE ? "White" : "Black"; + } + + private static ChessGame.Side opposite(ChessGame.Side side) { + return side == ChessGame.Side.WHITE ? ChessGame.Side.BLACK : ChessGame.Side.WHITE; + } + + private static String formatTime(long millis) { + if (millis < 10_000L) return String.format("%d.%d", millis / 1_000L, + (millis % 1_000L) / 100L); + long seconds = (millis + 999L) / 1_000L; + return String.format("%d:%02d", seconds / 60L, seconds % 60L); + } + + private void loadImages() { + for (ChessGame.Side side : ChessGame.Side.values()) { + Map perSide = + new EnumMap<>(ChessGame.PieceType.class); + images.put(side, perSide); + for (ChessGame.PieceType type : ChessGame.PieceType.values()) { + String pieceName = type == ChessGame.PieceType.KNIGHT ? "night" + : type.name().toLowerCase(); + String fileName = (side == ChessGame.Side.WHITE ? "W" : "B") + + pieceName + ".png"; + try (InputStream in = imageStream(fileName)) { + if (in != null) perSide.put(type, cropTransparent(ImageIO.read(in))); + } catch (IOException error) { + System.err.println("Could not load " + fileName + ": " + error.getMessage()); + } + } + } + } + + private static InputStream imageStream(String name) throws IOException { + InputStream resource = ChessApp.class.getResourceAsStream("/" + name); + if (resource != null) return resource; + Path local = Path.of(name); + if (Files.isRegularFile(local)) return Files.newInputStream(local); + Path assets = Path.of("assets", name); + if (Files.isRegularFile(assets)) return Files.newInputStream(assets); + return null; + } + + private static BufferedImage cropTransparent(BufferedImage image) { + if (image == null) return null; + int minX = image.getWidth(), minY = image.getHeight(), maxX = -1, maxY = -1; + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + if ((image.getRGB(x, y) >>> 24) > 8) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + } + return maxX < minX ? image : image.getSubimage(minX, minY, + maxX - minX + 1, maxY - minY + 1); + } + + private static final class ClockCard extends JPanel { + private static final long serialVersionUID = 1L; + private final JLabel name = new JLabel(); + private final JLabel time = new JLabel(); + private final String player; + private boolean active; + + ClockCard(String player) { + this.player = player; + setOpaque(false); + setLayout(new BorderLayout()); + setBorder(BorderFactory.createEmptyBorder(10, 13, 10, 13)); + setMaximumSize(new Dimension(Integer.MAX_VALUE, 74)); + setPreferredSize(new Dimension(240, 74)); + name.setFont(new Font("SansSerif", Font.BOLD, 11)); + name.setForeground(MUTED); + time.setHorizontalAlignment(SwingConstants.RIGHT); + time.setFont(new Font("Monospaced", Font.BOLD, 29)); + time.setForeground(TEXT); + add(name, BorderLayout.NORTH); + add(time, BorderLayout.CENTER); + } + + void update(long millis, boolean isActive) { + active = isActive; + name.setText(player + (active ? " · TO MOVE" : "")); + name.setForeground(active ? ACCENT : MUTED); + time.setText(formatTime(millis)); + time.setForeground(millis < 20_000L ? DANGER : TEXT); + repaint(); + } + + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g = (Graphics2D) graphics.create(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(active ? new Color(48, 69, 69) : PANEL_RAISED); + g.fillRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12); + g.setColor(active ? ACCENT : new Color(74, 92, 96)); + g.setStroke(new BasicStroke(active ? 2f : 1f)); + g.drawRoundRect(1, 1, getWidth() - 3, getHeight() - 3, 12, 12); + g.dispose(); + super.paintComponent(graphics); + } + } + + private static final class MoveRenderer extends DefaultListCellRenderer { + private static final long serialVersionUID = 1L; + @Override + public java.awt.Component getListCellRendererComponent(JList list, Object value, + int index, boolean selected, boolean focused) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, + index, selected, focused); + label.setBorder(BorderFactory.createEmptyBorder(6, 10, 6, 8)); + label.setBackground(selected ? new Color(64, 84, 84) + : index % 2 == 0 ? PANEL_RAISED : new Color(46, 64, 69)); + label.setForeground(TEXT); + return label; + } + } + + private static final class PaintButton extends JButton { + private static final long serialVersionUID = 1L; + enum Style { PRIMARY, SECONDARY, DANGER } + private final Style style; + + PaintButton(String label, Style style) { + super(label); + this.style = style; + setFont(new Font("SansSerif", Font.BOLD, 12)); + setOpaque(false); + setContentAreaFilled(false); + setBorderPainted(false); + setFocusPainted(false); + setRolloverEnabled(true); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + setPreferredSize(new Dimension(style == Style.PRIMARY ? 100 : 112, 36)); + getAccessibleContext().setAccessibleName(label); + } + + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g = (Graphics2D) graphics.create(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + Color fill = switch (style) { + case PRIMARY -> ACCENT; + case SECONDARY -> PANEL_RAISED; + case DANGER -> new Color(65, 52, 54); + }; + if (getModel().isRollover()) fill = fill.brighter(); + if (getModel().isPressed()) fill = fill.darker(); + if (!isEnabled()) fill = PANEL_RAISED; + g.setColor(fill); + g.fillRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 10, 10); + g.setColor(hasFocus() ? ACCENT : style == Style.DANGER ? DANGER : fill); + g.setStroke(new BasicStroke(hasFocus() ? 2f : 1f)); + g.drawRoundRect(1, 1, getWidth() - 3, getHeight() - 3, 10, 10); + g.setColor(style == Style.PRIMARY ? BACKGROUND : style == Style.DANGER ? DANGER : TEXT); + g.setFont(getFont()); + FontMetrics fm = g.getFontMetrics(); + int x = (getWidth() - fm.stringWidth(getText())) / 2; + int y = (getHeight() + fm.getAscent() - fm.getDescent()) / 2; + g.drawString(getText(), x, y); + g.dispose(); + } + } + + private static final class TimeButton extends JToggleButton { + private static final long serialVersionUID = 1L; + final TimeControl control; + + TimeButton(TimeControl control) { + super(control.label); + this.control = control; + setFont(new Font("SansSerif", Font.BOLD, 12)); + setOpaque(false); + setContentAreaFilled(false); + setBorderPainted(false); + setFocusPainted(false); + setRolloverEnabled(true); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + getAccessibleContext().setAccessibleDescription(control.minutes + " minutes with " + + control.incrementSeconds + " seconds added after each move"); + } + + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g = (Graphics2D) graphics.create(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + Color fill = isSelected() ? ACCENT : PANEL_RAISED; + if (getModel().isRollover() && isEnabled()) fill = fill.brighter(); + if (!isEnabled()) fill = isSelected() ? new Color(145, 125, 89) : PANEL_RAISED; + g.setColor(fill); + g.fillRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 9, 9); + if (hasFocus()) { + g.setColor(TEXT); + g.setStroke(new BasicStroke(2f)); + g.drawRoundRect(1, 1, getWidth() - 3, getHeight() - 3, 9, 9); + } + g.setFont(getFont()); + g.setColor(isSelected() ? BACKGROUND : isEnabled() ? TEXT : MUTED); + FontMetrics fm = g.getFontMetrics(); + g.drawString(getText(), (getWidth() - fm.stringWidth(getText())) / 2, + (getHeight() + fm.getAscent() - fm.getDescent()) / 2); + g.dispose(); + } + } + + private final class BoardPanel extends JPanel { + private static final long serialVersionUID = 1L; + private final Map scaledImages = new HashMap<>(); + private final Map outlines = new HashMap<>(); + private int cachedCell = -1; + private int originX; + private int originY; + private int cell; + private int focusRow = 6; + private int focusCol = 4; + + BoardPanel() { + setBackground(BACKGROUND); + setPreferredSize(new Dimension(670, 670)); + setFocusable(true); + getAccessibleContext().setAccessibleName("Chess board"); + getAccessibleContext().setAccessibleDescription( + "Use arrow keys to choose a square, Enter or Space to select or move, and Escape to clear selection."); + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent event) { + requestFocusInWindow(); + int[] square = squareAt(event.getX(), event.getY()); + if (square != null) { + focusRow = square[0]; + focusCol = square[1]; + selectSquare(square[0], square[1]); + repaint(); + } + } + }); + bindKey("UP", -1, 0); + bindKey("DOWN", 1, 0); + bindKey("LEFT", 0, -1); + bindKey("RIGHT", 0, 1); + getInputMap(WHEN_FOCUSED).put(javax.swing.KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "choose"); + getInputMap(WHEN_FOCUSED).put(javax.swing.KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "choose"); + getActionMap().put("choose", new javax.swing.AbstractAction() { + @Override public void actionPerformed(ActionEvent event) { + selectSquare(focusRow, focusCol); + } + }); + getInputMap(WHEN_FOCUSED).put(javax.swing.KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "clear"); + getActionMap().put("clear", new javax.swing.AbstractAction() { + @Override public void actionPerformed(ActionEvent event) { clearSelection(); } + }); + } + + private void bindKey(String key, int rowDelta, int colDelta) { + getInputMap(WHEN_FOCUSED).put(javax.swing.KeyStroke.getKeyStroke(key), key); + getActionMap().put(key, new javax.swing.AbstractAction() { + @Override public void actionPerformed(ActionEvent event) { + focusRow = Math.max(0, Math.min(7, focusRow + rowDelta)); + focusCol = Math.max(0, Math.min(7, focusCol + colDelta)); + repaint(); + } + }); + } + + private int[] squareAt(int x, int y) { + if (cell < 1 || x < originX || y < originY + || x >= originX + 8 * cell || y >= originY + 8 * cell) return null; + return new int[]{(y - originY) / cell, (x - originX) / cell}; + } + + @Override + protected void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + Graphics2D g = (Graphics2D) graphics.create(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + cell = Math.max(1, Math.min((getWidth() - 28) / 8, (getHeight() - 28) / 8)); + originX = (getWidth() - cell * 8) / 2; + originY = (getHeight() - cell * 8) / 2; + if (cachedCell != cell) { + scaledImages.clear(); + outlines.clear(); + cachedCell = cell; + } + g.setColor(new Color(8, 18, 22, 100)); + g.fillRoundRect(originX - 5, originY + 6, cell * 8 + 10, cell * 8 + 8, 8, 8); + + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 8; col++) { + int x = originX + col * cell, y = originY + row * cell; + Color square = (row + col) % 2 == 0 ? LIGHT_SQUARE : DARK_SQUARE; + g.setColor(square); + g.fillRect(x, y, cell, cell); + if (lastMove != null && ((lastMove.fromRow == row && lastMove.fromCol == col) + || (lastMove.toRow == row && lastMove.toCol == col))) { + g.setColor(new Color(231, 185, 87, 90)); + g.fillRect(x, y, cell, cell); + } + if (selectedRow == row && selectedCol == col) { + g.setColor(new Color(244, 197, 89, 125)); + g.fillRect(x, y, cell, cell); + } + ChessGame.Piece piece = game.pieceAt(row, col); + if (piece != null && piece.side == game.sideToMove() + && piece.type == ChessGame.PieceType.KING + && game.isInCheck(piece.side) && !isFinished()) { + g.setColor(new Color(207, 75, 69, 140)); + g.fillRect(x, y, cell, cell); + } + } + } + + for (ChessGame.Move move : selectedMoves) { + int x = originX + move.toCol * cell, y = originY + move.toRow * cell; + ChessGame.Piece target = game.pieceAt(move.toRow, move.toCol); + ChessGame.Piece selected = game.pieceAt(move.fromRow, move.fromCol); + boolean capture = target != null || (selected != null + && selected.type == ChessGame.PieceType.PAWN + && move.fromCol != move.toCol); + g.setColor(new Color(34, 54, 50, 130)); + if (capture) { + g.setStroke(new BasicStroke(Math.max(2f, cell * 0.055f))); + int inset = Math.max(5, cell / 12); + g.drawOval(x + inset, y + inset, cell - inset * 2, cell - inset * 2); + } else { + int diameter = Math.max(10, cell / 4); + g.fillOval(x + (cell - diameter) / 2, y + (cell - diameter) / 2, + diameter, diameter); + } + } + + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 8; col++) { + ChessGame.Piece piece = game.pieceAt(row, col); + if (piece != null) drawPiece(g, piece, originX + col * cell, originY + row * cell); + } + } + drawCoordinates(g); + if (hasFocus()) { + g.setColor(new Color(250, 205, 112, 220)); + g.setStroke(new BasicStroke(2f)); + g.drawRect(originX + focusCol * cell + 2, originY + focusRow * cell + 2, + cell - 5, cell - 5); + } + g.dispose(); + } + + private void drawCoordinates(Graphics2D g) { + g.setFont(new Font("SansSerif", Font.BOLD, Math.max(10, cell / 7))); + FontMetrics fm = g.getFontMetrics(); + for (int i = 0; i < 8; i++) { + g.setColor(i % 2 == 0 ? DARK_SQUARE.darker() : LIGHT_SQUARE); + g.drawString(Integer.toString(8 - i), originX + 4, + originY + i * cell + fm.getAscent() + 2); + g.setColor((7 + i) % 2 == 0 ? DARK_SQUARE.darker() : LIGHT_SQUARE); + String file = Character.toString((char) ('a' + i)); + g.drawString(file, originX + i * cell + cell - fm.stringWidth(file) - 5, + originY + 8 * cell - 5); + } + } + + private void drawPiece(Graphics2D g, ChessGame.Piece piece, int x, int y) { + BufferedImage source = images.get(piece.side).get(piece.type); + if (source == null) { + drawFallbackPiece(g, piece, x, y); + return; + } + String key = piece.side.name() + piece.type.name(); + BufferedImage scaled = scaledImages.computeIfAbsent(key, + ignored -> scalePiece(source, cell)); + BufferedImage outline = outlines.computeIfAbsent(key, + ignored -> silhouette(scaled, piece.side == ChessGame.Side.WHITE + ? new Color(35, 53, 52) : new Color(242, 236, 216))); + int px = x + (cell - scaled.getWidth()) / 2; + int py = y + (cell - scaled.getHeight()) / 2; + int radius = Math.max(1, cell / 48); + for (int oy = -radius; oy <= radius; oy++) { + for (int ox = -radius; ox <= radius; ox++) { + if (ox != 0 || oy != 0) g.drawImage(outline, px + ox, py + oy, null); + } + } + g.drawImage(scaled, px, py, null); + } + + private void drawFallbackPiece(Graphics2D g, ChessGame.Piece piece, int x, int y) { + String letter = piece.type == ChessGame.PieceType.KNIGHT ? "N" + : piece.type.name().substring(0, 1); + g.setFont(new Font("Serif", Font.BOLD, Math.max(19, cell * 2 / 3))); + FontMetrics fm = g.getFontMetrics(); + g.setColor(piece.side == ChessGame.Side.WHITE ? TEXT : BACKGROUND); + g.drawString(letter, x + (cell - fm.stringWidth(letter)) / 2, + y + (cell + fm.getAscent() - fm.getDescent()) / 2); + } + + private BufferedImage scalePiece(BufferedImage source, int square) { + int max = Math.max(1, square * 4 / 5); + double ratio = Math.min((double) max / source.getWidth(), + (double) max / source.getHeight()); + int width = Math.max(1, (int) Math.round(source.getWidth() * ratio)); + int height = Math.max(1, (int) Math.round(source.getHeight() * ratio)); + BufferedImage scaled = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = scaled.createGraphics(); + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + graphics.drawImage(source, 0, 0, width, height, null); + graphics.dispose(); + return scaled; + } + + private BufferedImage silhouette(BufferedImage source, Color color) { + BufferedImage result = new BufferedImage(source.getWidth(), source.getHeight(), + BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = result.createGraphics(); + graphics.drawImage(source, 0, 0, null); + graphics.setComposite(AlphaComposite.SrcIn); + graphics.setColor(color); + graphics.fillRect(0, 0, result.getWidth(), result.getHeight()); + graphics.dispose(); + return result; + } + } +} diff --git a/ChessGame.java b/ChessGame.java new file mode 100644 index 0000000..b593d96 --- /dev/null +++ b/ChessGame.java @@ -0,0 +1,432 @@ +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** The rules and state for one chess game, independent of its user interface. */ +public class ChessGame { + public enum Side { WHITE, BLACK } + public enum PieceType { KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN } + public enum Result { ONGOING, CHECKMATE, STALEMATE, DRAW_INSUFFICIENT_MATERIAL } + + /** An immutable piece; row zero is Black's home rank. */ + public static final class Piece { + public final Side side; + public final PieceType type; + + private Piece(Side side, PieceType type) { + this.side = side; + this.type = type; + } + } + + /** An immutable move. Promotions must name the new piece explicitly. */ + public static final class Move { + public final int fromRow; + public final int fromCol; + public final int toRow; + public final int toCol; + public final PieceType promotion; + + public Move(int fromRow, int fromCol, int toRow, int toCol, PieceType promotion) { + this.fromRow = fromRow; + this.fromCol = fromCol; + this.toRow = toRow; + this.toCol = toCol; + this.promotion = promotion; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof Move move)) return false; + return fromRow == move.fromRow && fromCol == move.fromCol + && toRow == move.toRow && toCol == move.toCol + && promotion == move.promotion; + } + + @Override + public int hashCode() { + return Objects.hash(fromRow, fromCol, toRow, toCol, promotion); + } + + @Override + public String toString() { + return "(" + fromRow + "," + fromCol + ") to (" + toRow + "," + toCol + ")" + + (promotion == null ? "" : "=" + promotion); + } + } + + private static final int[][] KNIGHT_STEPS = { + {-2, -1}, {-2, 1}, {-1, -2}, {-1, 2}, + {1, -2}, {1, 2}, {2, -1}, {2, 1} + }; + private static final int[][] DIAGONALS = {{-1, -1}, {-1, 1}, {1, -1}, {1, 1}}; + private static final int[][] STRAIGHTS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; + private static final PieceType[] PROMOTIONS = { + PieceType.QUEEN, PieceType.ROOK, PieceType.BISHOP, PieceType.KNIGHT + }; + + private Piece[][] board = new Piece[8][8]; + private Side turn; + private boolean whiteKingSide; + private boolean whiteQueenSide; + private boolean blackKingSide; + private boolean blackQueenSide; + private int enPassantRow; + private int enPassantCol; + + /** Starts with the standard chess position. */ + public ChessGame() { + reset(); + } + + /** Restores the standard position, including all special move rights. */ + public void reset() { + board = new Piece[8][8]; + PieceType[] home = { + PieceType.ROOK, PieceType.KNIGHT, PieceType.BISHOP, PieceType.QUEEN, + PieceType.KING, PieceType.BISHOP, PieceType.KNIGHT, PieceType.ROOK + }; + for (int col = 0; col < 8; col++) { + board[0][col] = new Piece(Side.BLACK, home[col]); + board[1][col] = new Piece(Side.BLACK, PieceType.PAWN); + board[6][col] = new Piece(Side.WHITE, PieceType.PAWN); + board[7][col] = new Piece(Side.WHITE, home[col]); + } + turn = Side.WHITE; + whiteKingSide = whiteQueenSide = blackKingSide = blackQueenSide = true; + enPassantRow = enPassantCol = -1; + } + + /** Returns the piece on a square, or null for an empty square. */ + public Piece pieceAt(int row, int col) { + requireSquare(row, col); + return board[row][col]; + } + + /** Returns the side whose move is next. */ + public Side sideToMove() { + return turn; + } + + /** Returns every legal move from the square for the side to move. */ + public List legalMovesFrom(int row, int col) { + if (!onBoard(row, col)) return List.of(); + Piece piece = board[row][col]; + if (piece == null || piece.side != turn) return List.of(); + List legal = new ArrayList<>(); + for (Move move : pseudoMoves(row, col, piece)) { + Piece[][] next = copyBoard(); + moveOnBoard(next, move); + if (!isInCheck(next, turn)) legal.add(move); + } + return List.copyOf(legal); + } + + /** Returns all legal moves for the side to move. */ + public List legalMoves() { + List moves = new ArrayList<>(); + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 8; col++) { + if (board[row][col] != null && board[row][col].side == turn) { + moves.addAll(legalMovesFrom(row, col)); + } + } + } + return List.copyOf(moves); + } + + /** Applies a legal move and returns true, or leaves the position unchanged and returns false. */ + public boolean makeMove(Move move) { + if (move == null || !onBoard(move.fromRow, move.fromCol) + || result() != Result.ONGOING + || !legalMovesFrom(move.fromRow, move.fromCol).contains(move)) return false; + + Piece moving = board[move.fromRow][move.fromCol]; + Piece captured = board[move.toRow][move.toCol]; + updateCastlingRights(moving, move, captured); + moveOnBoard(board, move); + enPassantRow = enPassantCol = -1; + if (moving.type == PieceType.PAWN && Math.abs(move.toRow - move.fromRow) == 2) { + enPassantRow = (move.toRow + move.fromRow) / 2; + enPassantCol = move.fromCol; + } + turn = opposite(turn); + return true; + } + + /** Returns whether the named side's king is attacked. */ + public boolean isInCheck(Side side) { + Objects.requireNonNull(side, "side"); + return isInCheck(board, side); + } + + /** Returns the current game result. */ + public Result result() { + if (legalMoves().isEmpty()) return isInCheck(turn) ? Result.CHECKMATE : Result.STALEMATE; + if (hasInsufficientMaterial()) return Result.DRAW_INSUFFICIENT_MATERIAL; + return Result.ONGOING; + } + + private List pseudoMoves(int row, int col, Piece piece) { + List moves = new ArrayList<>(); + switch (piece.type) { + case PAWN -> pawnMoves(moves, row, col, piece.side); + case KNIGHT -> { + for (int[] step : KNIGHT_STEPS) { + addIfAvailable(moves, row, col, row + step[0], col + step[1], piece.side); + } + } + case BISHOP -> addSlidingMoves(moves, row, col, piece.side, DIAGONALS); + case ROOK -> addSlidingMoves(moves, row, col, piece.side, STRAIGHTS); + case QUEEN -> { + addSlidingMoves(moves, row, col, piece.side, DIAGONALS); + addSlidingMoves(moves, row, col, piece.side, STRAIGHTS); + } + case KING -> { + for (int dr = -1; dr <= 1; dr++) { + for (int dc = -1; dc <= 1; dc++) { + if (dr != 0 || dc != 0) { + addIfAvailable(moves, row, col, row + dr, col + dc, piece.side); + } + } + } + addCastlingMoves(moves, row, col, piece.side); + } + } + return moves; + } + + private void pawnMoves(List moves, int row, int col, Side side) { + int direction = side == Side.WHITE ? -1 : 1; + int start = side == Side.WHITE ? 6 : 1; + int next = row + direction; + if (onBoard(next, col) && board[next][col] == null) { + addPawnMove(moves, row, col, next, col); + int doubleStep = row + 2 * direction; + if (row == start && board[doubleStep][col] == null) { + moves.add(new Move(row, col, doubleStep, col, null)); + } + } + for (int delta : new int[]{-1, 1}) { + int targetCol = col + delta; + if (!onBoard(next, targetCol)) continue; + Piece target = board[next][targetCol]; + if (target != null && target.side != side && target.type != PieceType.KING) { + addPawnMove(moves, row, col, next, targetCol); + } else if (next == enPassantRow && targetCol == enPassantCol && target == null) { + Piece victim = board[row][targetCol]; + if (victim != null && victim.side != side && victim.type == PieceType.PAWN) { + moves.add(new Move(row, col, next, targetCol, null)); + } + } + } + } + + private void addPawnMove(List moves, int fromRow, int fromCol, int toRow, int toCol) { + if (toRow == 0 || toRow == 7) { + for (PieceType promotion : PROMOTIONS) { + moves.add(new Move(fromRow, fromCol, toRow, toCol, promotion)); + } + } else { + moves.add(new Move(fromRow, fromCol, toRow, toCol, null)); + } + } + + private void addIfAvailable(List moves, int row, int col, int toRow, int toCol, Side side) { + if (!onBoard(toRow, toCol)) return; + Piece target = board[toRow][toCol]; + if (target == null || (target.side != side && target.type != PieceType.KING)) { + moves.add(new Move(row, col, toRow, toCol, null)); + } + } + + private void addSlidingMoves(List moves, int row, int col, Side side, int[][] directions) { + for (int[] direction : directions) { + int toRow = row + direction[0]; + int toCol = col + direction[1]; + while (onBoard(toRow, toCol)) { + Piece target = board[toRow][toCol]; + if (target == null) { + moves.add(new Move(row, col, toRow, toCol, null)); + } else { + if (target.side != side && target.type != PieceType.KING) { + moves.add(new Move(row, col, toRow, toCol, null)); + } + break; + } + toRow += direction[0]; + toCol += direction[1]; + } + } + } + + private void addCastlingMoves(List moves, int row, int col, Side side) { + int home = side == Side.WHITE ? 7 : 0; + if (row != home || col != 4 || isInCheck(board, side)) return; + Side attacker = opposite(side); + boolean kingSide = side == Side.WHITE ? whiteKingSide : blackKingSide; + boolean queenSide = side == Side.WHITE ? whiteQueenSide : blackQueenSide; + if (kingSide && isRook(home, 7, side) && board[home][5] == null && board[home][6] == null + && !isSquareAttacked(board, home, 5, attacker) + && !isSquareAttacked(board, home, 6, attacker)) { + moves.add(new Move(home, 4, home, 6, null)); + } + if (queenSide && isRook(home, 0, side) && board[home][1] == null + && board[home][2] == null && board[home][3] == null + && !isSquareAttacked(board, home, 3, attacker) + && !isSquareAttacked(board, home, 2, attacker)) { + moves.add(new Move(home, 4, home, 2, null)); + } + } + + private boolean isRook(int row, int col, Side side) { + Piece piece = board[row][col]; + return piece != null && piece.side == side && piece.type == PieceType.ROOK; + } + + private void updateCastlingRights(Piece moving, Move move, Piece captured) { + if (moving.type == PieceType.KING) { + if (moving.side == Side.WHITE) whiteKingSide = whiteQueenSide = false; + else blackKingSide = blackQueenSide = false; + } + if (moving.type == PieceType.ROOK) removeRookRight(moving.side, move.fromRow, move.fromCol); + if (captured != null && captured.type == PieceType.ROOK) { + removeRookRight(captured.side, move.toRow, move.toCol); + } + } + + private void removeRookRight(Side side, int row, int col) { + if (side == Side.WHITE && row == 7) { + if (col == 0) whiteQueenSide = false; + if (col == 7) whiteKingSide = false; + } else if (side == Side.BLACK && row == 0) { + if (col == 0) blackQueenSide = false; + if (col == 7) blackKingSide = false; + } + } + + private void moveOnBoard(Piece[][] position, Move move) { + Piece piece = position[move.fromRow][move.fromCol]; + if (piece.type == PieceType.PAWN && move.fromCol != move.toCol + && position[move.toRow][move.toCol] == null) { + position[move.fromRow][move.toCol] = null; // en passant + } + position[move.toRow][move.toCol] = move.promotion == null + ? piece : new Piece(piece.side, move.promotion); + position[move.fromRow][move.fromCol] = null; + if (piece.type == PieceType.KING && Math.abs(move.toCol - move.fromCol) == 2) { + int rookFrom = move.toCol == 6 ? 7 : 0; + int rookTo = move.toCol == 6 ? 5 : 3; + position[move.fromRow][rookTo] = position[move.fromRow][rookFrom]; + position[move.fromRow][rookFrom] = null; + } + } + + private Piece[][] copyBoard() { + Piece[][] copy = new Piece[8][8]; + for (int row = 0; row < 8; row++) { + System.arraycopy(board[row], 0, copy[row], 0, 8); + } + return copy; + } + + private static boolean isInCheck(Piece[][] position, Side side) { + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 8; col++) { + Piece piece = position[row][col]; + if (piece != null && piece.side == side && piece.type == PieceType.KING) { + return isSquareAttacked(position, row, col, opposite(side)); + } + } + } + throw new IllegalStateException("Position has no " + side + " king"); + } + + private static boolean isSquareAttacked(Piece[][] position, int row, int col, Side bySide) { + int pawnSourceRow = row + (bySide == Side.WHITE ? 1 : -1); + for (int dc : new int[]{-1, 1}) { + if (isPiece(position, pawnSourceRow, col + dc, bySide, PieceType.PAWN)) return true; + } + for (int[] step : KNIGHT_STEPS) { + if (isPiece(position, row + step[0], col + step[1], bySide, PieceType.KNIGHT)) return true; + } + for (int dr = -1; dr <= 1; dr++) { + for (int dc = -1; dc <= 1; dc++) { + if ((dr != 0 || dc != 0) + && isPiece(position, row + dr, col + dc, bySide, PieceType.KING)) return true; + } + } + return attackedAlong(position, row, col, bySide, DIAGONALS, PieceType.BISHOP) + || attackedAlong(position, row, col, bySide, STRAIGHTS, PieceType.ROOK); + } + + private static boolean attackedAlong(Piece[][] position, int row, int col, Side bySide, + int[][] directions, PieceType type) { + for (int[] direction : directions) { + int scanRow = row + direction[0]; + int scanCol = col + direction[1]; + while (onBoard(scanRow, scanCol)) { + Piece piece = position[scanRow][scanCol]; + if (piece != null) { + if (piece.side == bySide && (piece.type == type || piece.type == PieceType.QUEEN)) { + return true; + } + break; + } + scanRow += direction[0]; + scanCol += direction[1]; + } + } + return false; + } + + private static boolean isPiece(Piece[][] position, int row, int col, Side side, PieceType type) { + if (!onBoard(row, col)) return false; + Piece piece = position[row][col]; + return piece != null && piece.side == side && piece.type == type; + } + + private boolean hasInsufficientMaterial() { + int minorPieces = 0; + int knights = 0; + int bishopSquareColor = -1; + boolean bishopsOnOneColor = true; + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 8; col++) { + Piece piece = board[row][col]; + if (piece == null || piece.type == PieceType.KING) continue; + switch (piece.type) { + case PAWN, ROOK, QUEEN -> { return false; } + case BISHOP -> { + minorPieces++; + int squareColor = (row + col) & 1; + if (bishopSquareColor >= 0 && squareColor != bishopSquareColor) { + bishopsOnOneColor = false; + } + bishopSquareColor = squareColor; + } + case KNIGHT -> { + minorPieces++; + knights++; + } + default -> { } + } + } + } + if (minorPieces <= 1) return true; // Bare kings, or one minor piece. + return knights == 0 && bishopsOnOneColor; + } + + private static Side opposite(Side side) { + return side == Side.WHITE ? Side.BLACK : Side.WHITE; + } + + private static boolean onBoard(int row, int col) { + return row >= 0 && row < 8 && col >= 0 && col < 8; + } + + private static void requireSquare(int row, int col) { + if (!onBoard(row, col)) throw new IllegalArgumentException("Square is outside the board"); + } +} diff --git a/ChessGameTest.java b/ChessGameTest.java new file mode 100644 index 0000000..f3daede --- /dev/null +++ b/ChessGameTest.java @@ -0,0 +1,267 @@ +import java.util.List; + +/** Dependency-free regression tests for the headless chess rules. */ +public final class ChessGameTest { + private static int testsRun; + + public static void main(String[] args) { + run("initial position and reset", ChessGameTest::initialPositionAndReset); + run("turns and illegal moves", ChessGameTest::turnsAndIllegalMoves); + run("blocked sliders and pawns", ChessGameTest::blockedSlidersAndPawns); + run("pawn capture", ChessGameTest::pawnCapture); + run("Fool's Mate", ChessGameTest::foolsMate); + run("en passant", ChessGameTest::enPassant); + run("castling", ChessGameTest::castling); + run("pinned knight", ChessGameTest::pinnedKnight); + run("castling into check", ChessGameTest::castlingIntoCheck); + run("promotion choices", ChessGameTest::promotionChoices); + System.out.println("All " + testsRun + " ChessGame tests passed."); + } + + private static void initialPositionAndReset() { + ChessGame game = new ChessGame(); + assertInitialPosition(game); + + play(game, 6, 4, 4, 4); // e2-e4 + play(game, 1, 4, 3, 4); // e7-e5 + game.reset(); + assertInitialPosition(game); + } + + private static void assertInitialPosition(ChessGame game) { + ChessGame.PieceType[] backRank = { + ChessGame.PieceType.ROOK, ChessGame.PieceType.KNIGHT, + ChessGame.PieceType.BISHOP, ChessGame.PieceType.QUEEN, + ChessGame.PieceType.KING, ChessGame.PieceType.BISHOP, + ChessGame.PieceType.KNIGHT, ChessGame.PieceType.ROOK + }; + int whiteCount = 0; + int blackCount = 0; + for (int col = 0; col < 8; col++) { + assertPiece(game, 7, col, ChessGame.Side.WHITE, backRank[col]); + assertPiece(game, 6, col, ChessGame.Side.WHITE, ChessGame.PieceType.PAWN); + assertPiece(game, 1, col, ChessGame.Side.BLACK, ChessGame.PieceType.PAWN); + assertPiece(game, 0, col, ChessGame.Side.BLACK, backRank[col]); + } + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 8; col++) { + ChessGame.Piece piece = game.pieceAt(row, col); + if (piece != null) { + if (piece.side == ChessGame.Side.WHITE) whiteCount++; + else blackCount++; + } + } + } + check(whiteCount == 16 && blackCount == 16, "opening must have 16 pieces per side"); + check(game.sideToMove() == ChessGame.Side.WHITE, "white must move first"); + check(game.result() == ChessGame.Result.ONGOING, "opening must be ongoing"); + check(!game.isInCheck(ChessGame.Side.WHITE), "white starts out of check"); + check(!game.isInCheck(ChessGame.Side.BLACK), "black starts out of check"); + check(game.legalMoves().size() == 20, "opening must have 20 legal moves"); + } + + private static void turnsAndIllegalMoves() { + ChessGame game = new ChessGame(); + ChessGame.Move whiteKnight = move(7, 6, 5, 5); + check(!game.makeMove(move(1, 4, 3, 4)), "black cannot move first"); + check(!game.makeMove(move(7, 0, 5, 0)), "rook cannot jump over its pawn"); + check(game.pieceAt(1, 4).type == ChessGame.PieceType.PAWN, + "rejected move must leave the board unchanged"); + check(game.sideToMove() == ChessGame.Side.WHITE, + "rejected move must not change the turn"); + + play(game, 6, 4, 4, 4); + check(!game.makeMove(whiteKnight), "white cannot move twice in a row"); + check(game.pieceAt(7, 6).type == ChessGame.PieceType.KNIGHT, + "rejected second move must leave the knight in place"); + check(game.sideToMove() == ChessGame.Side.BLACK, + "rejected second move must preserve black's turn"); + play(game, 1, 4, 3, 4); + check(game.sideToMove() == ChessGame.Side.WHITE, "turn must return to white"); + } + + private static void blockedSlidersAndPawns() { + ChessGame game = new ChessGame(); + check(game.legalMovesFrom(7, 0).isEmpty(), "a1 rook starts blocked"); + check(game.legalMovesFrom(7, 2).isEmpty(), "c1 bishop starts blocked"); + check(game.legalMovesFrom(6, 4).size() == 2, + "e2 pawn starts with one-step and two-step moves"); + check(!game.legalMovesFrom(6, 4).contains(move(6, 4, 5, 3)), + "pawn cannot capture an empty square"); + check(!game.makeMove(move(6, 4, 3, 4)), "pawn cannot move three squares"); + + play(game, 6, 4, 4, 4); + play(game, 1, 4, 3, 4); + check(game.legalMovesFrom(4, 4).isEmpty(), + "e4 pawn cannot advance onto the opposing e5 pawn"); + check(!game.makeMove(move(4, 4, 3, 4)), + "blocked pawn advance must be rejected"); + } + + private static void pawnCapture() { + ChessGame game = new ChessGame(); + play(game, 6, 4, 4, 4); // e2-e4 + play(game, 1, 3, 3, 3); // d7-d5 + play(game, 4, 4, 3, 3); // e4xd5 + assertPiece(game, 3, 3, ChessGame.Side.WHITE, ChessGame.PieceType.PAWN); + check(game.pieceAt(4, 4) == null, "capturing pawn must leave its origin"); + check(game.sideToMove() == ChessGame.Side.BLACK, "capture must pass the turn"); + } + + private static void foolsMate() { + ChessGame game = new ChessGame(); + play(game, 6, 5, 5, 5); // f2-f3 + play(game, 1, 4, 3, 4); // e7-e5 + play(game, 6, 6, 4, 6); // g2-g4 + play(game, 0, 3, 4, 7); // Qd8-h4# + check(game.isInCheck(ChessGame.Side.WHITE), "Fool's Mate checks white"); + check(game.result() == ChessGame.Result.CHECKMATE, "Fool's Mate is checkmate"); + check(game.legalMoves().isEmpty(), "checkmated side has no legal moves"); + check(!game.makeMove(move(6, 0, 5, 0)), "no move is allowed after checkmate"); + } + + private static void enPassant() { + ChessGame game = enPassantPosition(); + check(game.legalMovesFrom(3, 4).contains(move(3, 4, 2, 3)), + "e5xd6 en passant must be legal immediately"); + play(game, 3, 4, 2, 3); + assertPiece(game, 2, 3, ChessGame.Side.WHITE, ChessGame.PieceType.PAWN); + check(game.pieceAt(3, 3) == null, + "en passant must remove the pawn from d5"); + + game = enPassantPosition(); + play(game, 7, 6, 5, 5); // White declines en passant with Ng1-f3. + play(game, 0, 1, 2, 2); // Black responds Nb8-c6. + check(!game.legalMovesFrom(3, 4).contains(move(3, 4, 2, 3)), + "en passant right must expire after one turn"); + } + + private static ChessGame enPassantPosition() { + ChessGame game = new ChessGame(); + play(game, 6, 4, 4, 4); // e2-e4 + play(game, 1, 0, 2, 0); // a7-a6 + play(game, 4, 4, 3, 4); // e4-e5 + play(game, 1, 3, 3, 3); // d7-d5 + return game; + } + + private static void castling() { + ChessGame game = new ChessGame(); + play(game, 7, 6, 5, 5); // Ng1-f3 + play(game, 0, 6, 2, 5); // Ng8-f6 + play(game, 6, 4, 4, 4); // e2-e4 + play(game, 1, 4, 3, 4); // e7-e5 + play(game, 7, 5, 6, 4); // Bf1-e2 + play(game, 0, 5, 1, 4); // Bf8-e7 + + check(game.legalMovesFrom(7, 4).contains(move(7, 4, 7, 6)), + "white kingside castling must be legal after clearing the path"); + play(game, 7, 4, 7, 6); + assertPiece(game, 7, 6, ChessGame.Side.WHITE, ChessGame.PieceType.KING); + assertPiece(game, 7, 5, ChessGame.Side.WHITE, ChessGame.PieceType.ROOK); + check(game.pieceAt(7, 4) == null && game.pieceAt(7, 7) == null, + "white king and rook must leave their original squares"); + + check(game.legalMovesFrom(0, 4).contains(move(0, 4, 0, 6)), + "black kingside castling must also be legal"); + play(game, 0, 4, 0, 6); + assertPiece(game, 0, 6, ChessGame.Side.BLACK, ChessGame.PieceType.KING); + assertPiece(game, 0, 5, ChessGame.Side.BLACK, ChessGame.PieceType.ROOK); + } + + private static void pinnedKnight() { + ChessGame game = new ChessGame(); + play(game, 6, 3, 4, 3); // d2-d4 clears the d2 square behind the knight. + play(game, 1, 4, 2, 4); // e7-e6 opens the black bishop's diagonal. + play(game, 7, 1, 5, 2); // Nb1-c3 + play(game, 0, 5, 4, 1); // Bf8-b4 pins the knight to the king. + check(!game.isInCheck(ChessGame.Side.WHITE), "the knight currently blocks check"); + check(game.legalMovesFrom(5, 2).isEmpty(), "pinned knight cannot expose its king"); + check(!game.makeMove(move(5, 2, 3, 1)), "pinned knight move must be rejected"); + assertPiece(game, 5, 2, ChessGame.Side.WHITE, ChessGame.PieceType.KNIGHT); + } + + private static void castlingIntoCheck() { + ChessGame game = new ChessGame(); + play(game, 6, 5, 5, 5); // f2-f3 opens the diagonal to g1. + play(game, 1, 4, 3, 4); // e7-e5 + play(game, 7, 6, 5, 7); // Ng1-h3 clears g1. + play(game, 0, 5, 3, 2); // Bf8-c5 attacks g1. + play(game, 6, 4, 4, 4); // e2-e4 clears e2 for the white bishop. + play(game, 1, 0, 2, 0); // a7-a6 + play(game, 7, 5, 6, 4); // Bf1-e2 clears f1. + play(game, 2, 0, 3, 0); // a6-a5 + + check(!game.isInCheck(ChessGame.Side.WHITE), "white king is not yet in check"); + check(!game.legalMovesFrom(7, 4).contains(move(7, 4, 7, 6)), + "white cannot castle onto attacked g1"); + check(!game.makeMove(move(7, 4, 7, 6)), "castling into check must be rejected"); + assertPiece(game, 7, 4, ChessGame.Side.WHITE, ChessGame.PieceType.KING); + assertPiece(game, 7, 7, ChessGame.Side.WHITE, ChessGame.PieceType.ROOK); + } + + private static void promotionChoices() { + ChessGame game = new ChessGame(); + play(game, 6, 1, 4, 1); // b2-b4 + play(game, 1, 0, 3, 0); // a7-a5 + play(game, 4, 1, 3, 0); // b4xa5 + play(game, 1, 1, 2, 1); // b7-b6 + play(game, 3, 0, 2, 1); // a5xb6 + play(game, 1, 6, 2, 6); // g7-g6 + play(game, 2, 1, 1, 1); // b6-b7 + play(game, 2, 6, 3, 6); // g6-g5 + + ChessGame.PieceType[] choices = { + ChessGame.PieceType.QUEEN, ChessGame.PieceType.ROOK, + ChessGame.PieceType.BISHOP, ChessGame.PieceType.KNIGHT + }; + for (ChessGame.PieceType choice : choices) { + check(game.legalMovesFrom(1, 1).contains( + new ChessGame.Move(1, 1, 0, 0, choice)), + "promotion to " + choice + " must be legal"); + } + check(!game.makeMove(move(1, 1, 0, 0)), + "promotion without a chosen piece must be rejected"); + check(game.makeMove(new ChessGame.Move(1, 1, 0, 0, ChessGame.PieceType.QUEEN)), + "chosen queen promotion must succeed"); + assertPiece(game, 0, 0, ChessGame.Side.WHITE, ChessGame.PieceType.QUEEN); + check(game.pieceAt(1, 1) == null, "promoted pawn must leave b7"); + } + + private static ChessGame.Move move(int fromRow, int fromCol, int toRow, int toCol) { + return new ChessGame.Move(fromRow, fromCol, toRow, toCol, null); + } + + private static void play(ChessGame game, int fromRow, int fromCol, int toRow, int toCol) { + ChessGame.Move move = move(fromRow, fromCol, toRow, toCol); + List legalMoves = game.legalMovesFrom(fromRow, fromCol); + check(legalMoves.contains(move), "expected legal move " + moveDescription(move)); + check(game.makeMove(move), "could not make legal move " + moveDescription(move)); + } + + private static void assertPiece(ChessGame game, int row, int col, + ChessGame.Side side, ChessGame.PieceType type) { + ChessGame.Piece piece = game.pieceAt(row, col); + check(piece != null && piece.side == side && piece.type == type, + "expected " + side + " " + type + " at (" + row + ", " + col + ")"); + } + + private static String moveDescription(ChessGame.Move move) { + return "(" + move.fromRow + ", " + move.fromCol + ") to (" + + move.toRow + ", " + move.toCol + ")"; + } + + private static void check(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void run(String name, Runnable test) { + try { + test.run(); + testsRun++; + System.out.println("PASS " + name); + } catch (AssertionError error) { + throw new AssertionError("FAIL " + name + ": " + error.getMessage(), error); + } + } +} diff --git a/Chessboard.java b/Chessboard.java deleted file mode 100644 index 2f67738..0000000 --- a/Chessboard.java +++ /dev/null @@ -1,213 +0,0 @@ -import javax.swing.*; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.jar.JarEntry; - -public class Chessboard extends JPanel - -{ - private static BufferedImage blurredImage; - - public static String position; - - static Color color = Color.decode("#769656"); - - public static String[][] board = new String[8][8]; - - Chessboard() { - - initializeBoard(); - - } - - private void initializeBoard() { - ImageCreater.resizeImagesandsetIcon("pawn"); - ImageCreater.resizeImagesandsetIcon("king"); - ImageCreater.resizeImagesandsetIcon("queen"); - ImageCreater.resizeImagesandsetIcon("bishop"); - ImageCreater.resizeImagesandsetIcon("rook"); - ImageCreater.resizeImagesandsetIcon("night"); - for (int i = 0; i < 2; i++) { - new Piece(0, 4, "king", false); - new Piece(7, 4, "king", true); - new Piece(7, 3, "queen", true); - new Piece(0, 3, "queen", false); - } - for (int i = 0; i < 8; i++) { - new Piece(1, i, "pawn", false); - } - for (int i = 0; i < 8; i++) { - new Piece(6, i, "pawn", true); - } - new Piece(0, 0, "rook", false); - new Piece(0, 7, "rook", false); - new Piece(7, 7, "rook", true); - new Piece(7, 0, "rook", true); - new Piece(0, 2, "bishop", false); - new Piece(0, 5, "bishop", false); - new Piece(7, 2, "bishop", true); - new Piece(7, 5, "bishop", true); - - new Piece(0, 1, "night", false); - new Piece(0, 6, "night", false); - new Piece(7, 1, "night", true); - new Piece(7, 6, "night", true); - } - - protected void paintComponent(Graphics g) { // Graphics2d g2d = (Graphics2d) g; - super.paintComponent(g); - Graphics2D g2d = (Graphics2D) g.create(); - int[] x = { 0, 0, 800, 800, 0 }; - int[] y = { 0, 800, 800, 0, 0 }; - - // Piece.setIcons(); - g2d.setColor(color); - g2d.drawPolyline(x, y, 5); - - g2d.setColor(color); - for (int j = 0; j < 8; j++) { - for (int i = 0; i < 8; i++) { - if (j % 2 == 0) { - if (i % 2 != 0) { - g2d.fillRect(i * 100, j * 100, 100, 100); - } - } - - else if (j % 2 != 0) { - if (i % 2 == 0) { - g2d.fillRect(i * 100, j * 100, 100, 100); - } - } - } - } - g2d.setColor(Color.decode("#FFFDD0")); - for (int j = 0; j < 8; j++) { - for (int i = 0; i < 8; i++) { - if (j % 2 == 0) { - if (i % 2 == 0) { - g2d.fillRect(i * 100, j * 100, 100, 100); - } - } - - else if (j % 2 != 0) { - if (i % 2 != 0) { - g2d.fillRect(i * 100, j * 100, 100, 100); - } - } - } - } - - for (int j = 0; j < 8; j++) { - for (int i = 0; i < 8; i++) { - g2d.setColor(Color.decode("#ffd692")); - if (Piece.posiblemove[j][i] != null) { - if(Piece.posiblemove[j][i] == "Posible"){ - g2d.fillRect(i * 100, j * 100, 100, 100); - g2d.setColor(Color.decode("#D3D3D3")); - g2d.fillOval(i * 100 + 30, j * 100 + 30, 40, 40); - - } - - - else if (Piece.posiblemove[j][i] =="Capture") - { - Stroke stroke = new BasicStroke(3.0f); - g2d.setStroke(stroke); - g2d.drawOval(i*100+10, j*100+10, 90, 90); - } - } - } - } - for (int i = 0; i < 8; i++) { - for (int p = 0; p < 8; p++) { - if (board[p][i] == "Wpawn") { - Image image = ImageCreater.imgwhitepawn.getImage();g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Bpawn") { - Image image = ImageCreater.imgblackpawn.getImage();g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Wking") { - Image image = ImageCreater.imgwhiteking.getImage();g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Bking") { - Image image = ImageCreater.imgblackking.getImage(); g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Wqueen") { - Image image = ImageCreater.imgwhitequeen.getImage(); g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Bqueen") { - Image image = ImageCreater.imgblackqueen.getImage(); g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Wbishop") { - Image image = ImageCreater.imgwhitebishop.getImage(); g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Bbishop") { - Image image = ImageCreater.imgblackbishop.getImage(); g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Wrook") { - Image image = ImageCreater.imgwhiterook.getImage(); g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Brook") { - Image image = ImageCreater.imgblackrook.getImage(); g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Wnight") { - Image image = ImageCreater.imgwhiteknight.getImage(); g2d.drawImage(image, i * 100, p * 100, null); - } - if (board[p][i] == "Bnight") { - Image image = ImageCreater.imgblackknight.getImage(); g2d.drawImage(image, i * 100, p * 100, null); - } - } - } - for (int i=0 ; i<800 ;i+=100){ - g2d.setColor(Color.BLACK); //Vertical line - g2d.drawLine(i, 0, i, 800); - - } - for (int j = 0 ; j<800;j+=100){ - g2d.setColor(Color.BLACK); //Horizontal - g2d.drawLine(0,j,800,j); - } - } - - public static void checkwincodition(boolean playerwin, String condition) { - JDialog announcement = new JDialog(); - announcement.setLayout(new BorderLayout()); - - JLabel label = new JLabel(); - label.setFont(new Font("Arial", Font.BOLD, 20)); - label.setHorizontalAlignment(SwingConstants.CENTER); - - if (playerwin && condition.equals("resign")) { - label.setText("White wins"); - label.setBackground(Color.WHITE); - label.setForeground(Color.BLACK); - Chessclock.checkstatus(true, true); - } else if (!playerwin && condition.equals("resign")) { - label.setText("Black wins"); - label.setBackground(Color.BLACK); - label.setForeground(Color.WHITE); - Chessclock.checkstatus(true, true); - } else if (playerwin && condition.equals("draw")) { - label.setText("Draw"); - label.setBackground(Color.WHITE); - label.setForeground(Color.BLACK); - Chessclock.checkstatus(true, true); - } else { - - label.setText(""); - } - - label.setOpaque(true); - - announcement.add(label, BorderLayout.CENTER); - - announcement.setTitle("Game Over"); - announcement.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - announcement.setSize(300, 150); - announcement.setLocationRelativeTo(null); - announcement.setModal(true); - announcement.setVisible(true); - - } -} diff --git a/Chessclock.java b/Chessclock.java deleted file mode 100644 index 4d17e68..0000000 --- a/Chessclock.java +++ /dev/null @@ -1,186 +0,0 @@ -import javax.swing.JPanel; -import java.awt.*; -import javax.swing.*; -import java.awt.event.*; - -public class Chessclock extends JPanel implements ActionListener { - static boolean Gamestart = false; - - private JButton bullet3 = new JButton("3|2"); - private JButton blitz3 = new JButton("3"); - private JButton blitz5 = new JButton("5|5"); - private JButton Rapid10 = new JButton("10"); - - static int remainingSeconds1; - static int remainingSeconds2; - - private static Timer timer1; - private static Timer timer2; - - private JLabel player1Label; - private JLabel player2Label; - - public int minutes1 = 0; - public int seconds1 = 0; - - public int minutes2 = 0; - public int seconds2 = 0; - public static int constantsec; - - public int totalSeconds1; - public int totalSeconds2; - - public Chessclock(JLabel player1, JLabel player2) { - this.setLayout(new GridLayout(2, 2)); - this.add(bullet3); - this.add(blitz3); - this.add(blitz5); - this.add(Rapid10); - - bullet3.addActionListener(this); - blitz3.addActionListener(this); - blitz5.addActionListener(this); - Rapid10.addActionListener(this); - - // Initialize the player labels - // player1Label = new JLabel("", SwingConstants.CENTER); - this.player1Label = player1; - this.player2Label = player2; - player1Label.setFont(new Font("Arial", Font.PLAIN, 48)); - // player1Label = new JLabel(); - - // player2Label = new JLabel("", SwingConstants.CENTER); - player2Label.setFont(new Font("Arial", Font.PLAIN, 48)); - // player2Label = new JLabel(); - - } - - public void actionPerformed(ActionEvent e) { - JButton btn = (JButton) e.getSource(); - - switch (btn.getText()) { - case "3|2": - minutes1 = 3; - seconds1 = 2; - minutes2 = 3; - seconds2 = 2; - this.constantsec = 2; - break; - case "3": - minutes1 = 3; - seconds1 = 0; - minutes2 = 3; - seconds2 = 0; - this.constantsec = 0; - break; - case "5|5": - minutes1 = 5; - seconds1 = 5; - minutes2 = 5; - seconds2 = 5; - this.constantsec = 5; - break; - case "10": - minutes1 = 10; - seconds1 = 0; - minutes2 = 10; - seconds2 = 0; - this.constantsec = 0; - break; - - } - player1Label.setText(String.format("%02d:%02d", minutes1, seconds1)); - player2Label.setText(String.format("%02d:%02d", minutes2, seconds2)); - JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this); - frame.dispose(); - - Gamestart = true; - Chessboard game = new Chessboard(); - checkclock(); - - } - - public void checkclock() { - - int totalSeconds1 = minutes1 * 60 + seconds1; - int totalSeconds2 = minutes2 * 60 + seconds2; - timer1 = new Timer(1000, new ActionListener() { - int remainingSeconds1 = totalSeconds1; - - public void actionPerformed(ActionEvent evt) { - remainingSeconds1--; - int remainingMinutes = remainingSeconds1 / 60; - int remainingSecondsDisplay = remainingSeconds1 % 60; - player1Label.setText(String.format("%02d:%02d", remainingMinutes, remainingSecondsDisplay)); - if (remainingSeconds1 == 0) { - JOptionPane.showMessageDialog(null, "Time's up!"); - timer1.stop(); - // player1Label.setText(""); - } - } - }); - timer1.stop(); - - timer2 = new Timer(1000, new ActionListener() { - int remainingSeconds2 = totalSeconds2; - - public void actionPerformed(ActionEvent evt) { - remainingSeconds2--; - int remainingMinutes = remainingSeconds2 / 60; - int remainingSecondsDisplay = remainingSeconds2 % 60; - player2Label.setText(String.format("%02d:%02d", remainingMinutes, remainingSecondsDisplay)); - if (remainingSeconds2 == 0) { - JOptionPane.showMessageDialog(null, "Time's up!"); - timer2.stop(); - // player2Label.setText(""); - } - } - }); - timer2.stop(); - // Set the initial turn to player 1 - - } - - public static void addTimeToTimer1(Timer timer, int secondsToAdd) { - ActionListener[] listeners = timer.getActionListeners(); - for (ActionListener listener : listeners) { - if (listener instanceof Chessclock) { - Chessclock timerListener = (Chessclock) listener; - Chessclock.remainingSeconds1 += secondsToAdd; - break; - } - } - } - - public static void addTimeToTimer2(Timer timer, int secondsToAdd) { - ActionListener[] listeners = timer.getActionListeners(); - for (ActionListener listener : listeners) { - if (listener instanceof Chessclock) { - Chessclock timerListener = (Chessclock) listener; - Chessclock.remainingSeconds2 += secondsToAdd; - break; - } - } - } - - public static void checkstatus(boolean check, boolean check2) { - - if (check == true && check2 == true) { - timer1.stop(); - timer2.stop(); - } else if (check == false && check2 == false) { - // currentLabel = player2Label; - timer2.start(); - timer1.stop(); - addTimeToTimer1(timer1, constantsec); - } else if (check == true && check2 == false) { - // currentLabel = player1Label; - timer1.start(); - timer2.stop(); - remainingSeconds2 += constantsec; - addTimeToTimer2(timer2, constantsec); - } - - } - -} diff --git a/ImageCreater.java b/ImageCreater.java deleted file mode 100644 index ef6b89d..0000000 --- a/ImageCreater.java +++ /dev/null @@ -1,143 +0,0 @@ -import java.io.File; -import java.io.IOException; - -import javax.imageio.ImageIO; -import javax.swing.*; - -import java.awt.*; - -public class ImageCreater extends Chessboard { - static ImageIcon imgblackpawn; - static ImageIcon imgwhitepawn; - - static ImageIcon imgblackking; - static ImageIcon imgwhiteking; - - static ImageIcon imgblackqueen; - static ImageIcon imgwhitequeen; - - static ImageIcon imgblackbishop; - static ImageIcon imgwhitebishop; - - static ImageIcon imgblackrook; - static ImageIcon imgwhiterook; - - static ImageIcon imgblackknight; - static ImageIcon imgwhiteknight; - - public static void resizeImagesandsetIcon(String piece) { - - if (piece.charAt(0) == 'k') { - try { - // Load original PNG images - Image originalBlack = ImageIO.read(new File("Bking.png")); - Image originalWhite = ImageIO.read(new File("Wking.png")); - - // Resize images to 100x100 pixels - Image resizedBlack = originalBlack.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - Image resizedWhite = originalWhite.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - - // Create new ImageIcon objects from resized images - imgblackking = new ImageIcon(resizedBlack); - imgwhiteking = new ImageIcon(resizedWhite); - - // jlPicblack.setIcon(imgblackking); - // jlPicwhite.setIcon(imgwhiteking); - - } catch (IOException e) { - e.printStackTrace(); - } - } else if (piece.charAt(0) == 'p') { - try { - - Image originalBlack = ImageIO.read(new File("Bpawn.png")); - Image originalWhite = ImageIO.read(new File("Wpawn.png")); - - - Image resizedBlack = originalBlack.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - Image resizedWhite = originalWhite.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - - // Create new ImageIcon objects from resized images - imgblackpawn = new ImageIcon(resizedBlack); - imgwhitepawn = new ImageIcon(resizedWhite); - - - } catch (IOException e) { - e.printStackTrace(); - } - } else if (piece.charAt(0) == 'q') { - try { - - Image originalBlack = ImageIO.read(new File("Bqueen.png")); - Image originalWhite = ImageIO.read(new File("Wqueen.png")); - - - Image resizedBlack = originalBlack.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - Image resizedWhite = originalWhite.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - - - imgblackqueen = new ImageIcon(resizedBlack); - imgwhitequeen = new ImageIcon(resizedWhite); - - - } catch (IOException e) { - e.printStackTrace(); - } - } else if (piece.charAt(0) == 'b') { - try { - - Image originalBlack = ImageIO.read(new File("Bbishop.png")); - Image originalWhite = ImageIO.read(new File("Wbishop.png")); - - - Image resizedBlack = originalBlack.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - Image resizedWhite = originalWhite.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - - - imgblackbishop = new ImageIcon(resizedBlack); - imgwhitebishop = new ImageIcon(resizedWhite); - - - - } catch (IOException e) { - e.printStackTrace(); - } - } else if (piece.charAt(0) == 'r') { - try { - - Image originalBlack = ImageIO.read(new File("Brook.png")); - Image originalWhite = ImageIO.read(new File("Wrook.png")); - - - Image resizedBlack = originalBlack.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - Image resizedWhite = originalWhite.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - - - imgblackrook = new ImageIcon(resizedBlack); - imgwhiterook = new ImageIcon(resizedWhite); - - - } catch (IOException e) { - e.printStackTrace(); - } - } else if (piece.charAt(0) == 'n') { - try { - - Image originalBlack = ImageIO.read(new File("Bnight.png")); - Image originalWhite = ImageIO.read(new File("Wnight.png")); - - - Image resizedBlack = originalBlack.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - Image resizedWhite = originalWhite.getScaledInstance(100, 100, Image.SCALE_SMOOTH); - - - imgblackknight = new ImageIcon(resizedBlack); - imgwhiteknight = new ImageIcon(resizedWhite); - - - } catch (IOException e) { - e.printStackTrace(); - } - } - } -} diff --git a/MouseMotion.java b/MouseMotion.java deleted file mode 100644 index e31a6a5..0000000 --- a/MouseMotion.java +++ /dev/null @@ -1,179 +0,0 @@ -import javax.swing.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; - -public class MouseMotion implements MouseListener, MouseMotionListener, ActionListener { - public static int positionclickx; - public static int positionclicky; - JLabel labletest; - - int clicklocalx; - int clicklocaly; - int releasedlocalx; - int releasedlocaly; - static boolean turntomove = true; - JPanel temp; - - JButton resign1; - JButton resign2; - JButton draw1; - JButton draw2; - - boolean draw1accept; - boolean draw2accept; - - public boolean playerwin; - public boolean gameends; - - MouseMotion(JPanel test, JLabel labletest, JButton resign1, JButton resign2, JButton draw1, JButton draw2) { - this.labletest = labletest; - this.temp = test; - this.resign1 = resign1; - this.resign2 = resign2; - this.draw1 = draw1; - this.draw2 = draw2; - - resign1.addActionListener(this); - resign2.addActionListener(this); - draw1.addActionListener(this); - draw2.addActionListener(this); - } - - public void repaint(JPanel temp) { - this.temp = temp; - temp.getParent().repaint(); - temp.revalidate(); - } - - @Override - public void mousePressed(MouseEvent e) { - Piece check = new Piece(); - - this.clicklocalx = ((e.getX() - 9) / 100); - this.clicklocaly = ((e.getY() - 32) / 100); - - check.checklayoutboard(clicklocaly, clicklocalx); - if (Chessclock.Gamestart && gameends != true) { - if (Piece.Pieces != null) { - - if (Piece.Pieces.charAt(0) == 'B' && turntomove == false) { - Piece.posiblemove = new String[8][8]; - check.posiblemovecal(clicklocaly, clicklocalx); - labletest.setText(Piece.Pieces); - repaint(temp); - } else if (Piece.Pieces.charAt(0) == 'W' && turntomove == true) { - Piece.posiblemove = new String[8][8]; - check.posiblemovecal(clicklocaly, clicklocalx); - labletest.setText(Piece.Pieces); - repaint(temp); - } else { - check.posiblemovecal(clicklocaly, clicklocalx); - - labletest.setText(Piece.Pieces); - repaint(temp); - } - - } - } else { - labletest.setText("Please Follow the rules"); - } - } - - @Override - public void mouseReleased(MouseEvent e) { - - int releasedlocalx = ((e.getX() - 9) / 100); //implicit casting - int releasedlocaly = ((e.getY() - 32) / 100); //implicit casting - if (Piece.posiblemove[releasedlocaly][releasedlocalx] == "Posible" || Piece.posiblemove[releasedlocaly][releasedlocalx] =="Capture") { - if(Piece.Pieces != null){ - if (Piece.Pieces == "Bpawn" && releasedlocaly == 7) { - Piece.promotion(releasedlocaly, releasedlocalx, turntomove, clicklocaly, clicklocalx); - } else if (Piece.Pieces == "Wpawn" && releasedlocaly == 0) { - Piece.promotion(releasedlocaly, releasedlocalx, turntomove, clicklocaly, clicklocalx); - } else { - Chessboard.board[releasedlocaly][releasedlocalx] = Piece.Pieces; - - Chessboard.board[clicklocaly][clicklocalx] = null; - } - Piece.posiblemove = new String[8][8]; - - repaint(temp); - if (turntomove == true) { - turntomove = false; - Chessclock.checkstatus(turntomove, false); - } else { - turntomove = true; - Chessclock.checkstatus(turntomove, false); - } - } - } - - } - - @Override - public void mouseDragged(MouseEvent e) { - ; - } - - @Override - public void mouseMoved(MouseEvent e) { - - labletest.setText((int)(e.getX() - 9) + " " + (int)(e.getY() - 32)); - } - - @Override - public void mouseClicked(MouseEvent e) { - ; - } - - @Override - public void mouseEntered(MouseEvent e) { - ; - } - - @Override - public void mouseExited(MouseEvent e) { - ; - } - - @Override - public void actionPerformed(ActionEvent e) { - if (e.getSource() == resign1) { - playerwin = false; - gameends = true; - Chessboard.checkwincodition(playerwin, "resign"); - } else if (e.getSource() == resign2) { - playerwin = true; - gameends = true; - Chessboard.checkwincodition(playerwin, "resign"); - } else if (e.getSource() == draw1) { - - labletest.setText("Player 1 Offers a Draw"); - draw1accept = true; - playerwin = true; - if (draw1accept == true && draw2accept == true) { - Chessboard.checkwincodition(playerwin, "draw"); - draw1accept = false; - draw2accept = false; - gameends = true; - } - - } else if (e.getSource() == draw2) { - - labletest.setText("Player 2 Offers a Draw"); - draw2accept = true; - playerwin = true; - if (draw1accept == true && draw2accept == true) { - Chessboard.checkwincodition(playerwin, "draw"); - draw2accept = false; - draw1accept = false; - gameends = true; - - } - } - } - -} \ No newline at end of file diff --git a/Piece.java b/Piece.java deleted file mode 100644 index 92acee7..0000000 --- a/Piece.java +++ /dev/null @@ -1,451 +0,0 @@ - -import java.time.Period; - -import javax.swing.*; - -public class Piece { - // for future AI i - // public static final int KING = 1; - // public static final int QUEEN = 2; - // public static final int ROOK = 3; - // public static final int KNIGHT = 4; - // public static final int BISHOP = 5; - // public static final int PAWN = 6; - - // constant values for the pieces - // public static final int KING_VALUE = 1000000; - // public static final int QUEEN_VALUE = 9; - // public static final int ROOK_VALUE = 5; - // public static final int KNIGHT_VALUE = 3; - // public static final int BISHOP_VALUE = 3; - // public static final int PAWN_VALUE = 1; - - protected boolean color; - int Visibility; - static String Pieces; - - static boolean incheck = false; - - private static int[] scanXKQ = { -1, 0, 1, 1, 1, 0, -1, -1 }; - private static int[] scanYKQ = { 1, 1, 1, 0, -1, -1, -1, 0 }; - - private static int[] scanXpawn = { -1, 0, 1 }; - private static int[] scanYpawn = { 1, 1, 1 }; - - private static int[] scanXbis = { -1, 1, 1, -1 }; - private static int[] scanYbis = { 1, 1, -1, -1 }; - - private static int[] scanXrook = { 0, 1, 0, -1 }; - private static int[] scanYrook = { 1, 0, -1, 0 }; - - private static int[] scanXknight = { -2, -1, 1, 2, 2, 1, -1, -2 }; - private static int[] scanYknight = { 1, 2, 2, 1, -1, -2, -2, -1 }; - static String[][] posiblemove = new String[8][8]; - - public Piece() { - } - - public Piece(int x, int y, String type, boolean Color1) { - if (type == "pawn") { - Pawn(x, y, Color1); - } - if (type == "king") { - King(x, y, Color1); - } - if (type == "queen") { - Queen(x, y, Color1); - } - if (type == "bishop") { - Bishop(x, y, Color1); - } - if (type == "rook") { - Rook(x, y, Color1); - } - if (type == "night") { - Knight(x, y, Color1); - } - } - - public void Pawn(int x, int y, boolean Color1) { - if (Color1 == true) { - Chessboard.board[x][y] = "Wpawn"; - } else { - Chessboard.board[x][y] = "Bpawn"; - } - - } - - public void King(int x, int y, boolean Color1) { - - if (Color1 == true) { - Chessboard.board[x][y] = "Wking"; - } else { - Chessboard.board[x][y] = "Bking"; - } - - } - - public void Knight(int x, int y, boolean Color1) { - - if (Color1 == true) { - Chessboard.board[x][y] = "Wnight"; - } else { - Chessboard.board[x][y] = "Bnight"; - } - - } - - public void Queen(int x, int y, boolean Color1) { - if (Color1 == true) { - Chessboard.board[x][y] = "Wqueen"; - } else { - Chessboard.board[x][y] = "Bqueen"; - } - } - - public void Bishop(int x, int y, boolean Color1) { - if (Color1 == true) { - Chessboard.board[x][y] = "Wbishop"; - } else { - Chessboard.board[x][y] = "Bbishop"; - } - } - - public void Rook(int x, int y, boolean Color1) { - if (Color1 == true) { - Chessboard.board[x][y] = "Wrook"; - } else { - Chessboard.board[x][y] = "Brook"; - } - } - - public String checklayoutboard(int y, int x) { - - if (Chessboard.board[y][x] != null) { - Pieces = Chessboard.board[y][x]; - - if (Pieces.charAt(1) == 'p' || Pieces.charAt(1) == 'k') { - this.Visibility = 1; - if (Pieces.charAt(0) == 'W') { - this.color = true; - } else { - this.color = false; - } - } - if (Pieces.charAt(1) == 'q' || Pieces.charAt(1) == 'b' || Pieces.charAt(1) == 'r') { - this.Visibility = 8; - if (Pieces.charAt(0) == 'W') { - this.color = true; - } else { - this.color = false; - } - } - if (Pieces.charAt(1) == 'n') { - if (Pieces.charAt(0) == 'W') { - this.color = true; - } else { - this.color = false; - } - } - } - - else { - return (Pieces = null); - } - - return Pieces; - - } - - public void posiblemovecal(int y, int x) { - - if (this.color == false && MouseMotion.turntomove == false) { - - if (Pieces.charAt(1) == 'k' || Pieces.charAt(1) == 'q') { // King / Queen - for (int i = 0; i < scanXKQ.length; i++) { - for (int j = 1; j <= Visibility; j += 1) { - try { - - if (Chessboard.board[y + (scanYKQ[i] * j)][x + (scanXKQ[i] * j)] == null) { - posiblemove[y + (scanYKQ[i] * j)][x + (scanXKQ[i] * j)] = "Posible"; - } - - else if (Chessboard.board[y + (scanYKQ[i] * j)][x + (scanXKQ[i] * j)].charAt(0) == 'W') { - posiblemove[y + (scanYKQ[i] * j)][x + (scanXKQ[i] * j)] = "Posible"; - break; - } else if (Pieces.charAt(1) == 'k' || Pieces.charAt(1) == 'q') { - - if (Chessboard.board[y + (scanYKQ[i] * j)][x + (scanXKQ[i] * j)].charAt(0) == 'B') { - - break; - } - } - - } catch (ArrayIndexOutOfBoundsException e) { - continue; - } - - } - } - } else if (Pieces.charAt(1) == 'p') { // Pawn - for (int i = 0; i < scanXpawn.length; i++) { - for (int j = 1; j <= Visibility; j += 1) { - try { - - if (Chessboard.board[y + (scanYpawn[i] * j)][x + (scanXpawn[i] * j)] == null) { - if (scanXpawn[i] == 0) { - if (y == 1) { - posiblemove[y + (scanYpawn[i] * 2 * j)][x + (scanXpawn[i] * j)] = "Posible"; - } - - posiblemove[y + (scanYpawn[i] * j)][x + (scanXpawn[i] * j)] = "Posible"; - } - } else if (Chessboard.board[y + (scanYpawn[i] * j)][x + (scanXpawn[i] * j)] - .charAt(0) == 'W') { - if (scanXpawn[i] != 0) { - posiblemove[y + (scanYpawn[i] * j)][x + (scanXpawn[i] * j)] = "Capture"; - break; - } - } - } catch (ArrayIndexOutOfBoundsException e) { - continue; - } - - } - } - } else if (Pieces.charAt(1) == 'b') { - for (int i = 0; i < scanXbis.length; i++) { - for (int j = 1; j <= Visibility; j += 1) { - try { - - if (Chessboard.board[y + (scanYbis[i] * j)][x + (scanXbis[i] * j)] == null) { - posiblemove[y + (scanYbis[i] * j)][x + (scanXbis[i] * j)] = "Posible"; - } - - else if (Chessboard.board[y + (scanYbis[i] * j)][x + (scanXbis[i] * j)].charAt(0) == 'W') { - - posiblemove[y + (scanYbis[i] * j)][x + (scanXbis[i] * j)] = "Posible"; - break; - - } else if (Chessboard.board[y + (scanYbis[i] * j)][x + (scanXbis[i] * j)] - .charAt(0) == 'B') { - - break; - } - } catch (ArrayIndexOutOfBoundsException e) { - continue; - } - - } - } - } else if (Pieces.charAt(1) == 'r') { - for (int i = 0; i < scanXrook.length; i++) { - for (int j = 1; j <= Visibility; j += 1) { - try { - - if (Chessboard.board[y + (scanYrook[i] * j)][x + (scanXrook[i] * j)] == null) { - posiblemove[y + (scanYrook[i] * j)][x + (scanXrook[i] * j)] = "Posible"; - } - - else if (Chessboard.board[y + (scanYrook[i] * j)][x + (scanXrook[i] * j)] - .charAt(0) == 'W') { - - posiblemove[y + (scanYrook[i] * j)][x + (scanXrook[i] * j)] = "Posible"; - break; - - } else if (Chessboard.board[y + (scanYrook[i] * j)][x + (scanXrook[i] * j)] - .charAt(0) == 'B') { - - break; - } - } catch (ArrayIndexOutOfBoundsException e) { - continue; - } - - } - } - } - - else if (Pieces.charAt(1) == 'n') { - for (int i = 0; i < scanXknight.length; i++) { - - try { - - if (Chessboard.board[y + (scanYknight[i])][x + (scanXknight[i])] == null) { - posiblemove[y + (scanYknight[i])][x + (scanXknight[i])] = "Posible"; - } - - else if (Chessboard.board[y + (scanYknight[i])][x + (scanXknight[i])] - .charAt(0) == 'W') { - - posiblemove[y + (scanYknight[i])][x + (scanXknight[i])] = "Posible"; - continue; - - } else if (Chessboard.board[y + (scanYknight[i])][x + (scanXknight[i])] - .charAt(0) == 'B') { - - continue; - } - } catch (ArrayIndexOutOfBoundsException e) { - continue; - } - - } - - } - - } - - if (this.color == true && MouseMotion.turntomove == true) { - - if (Pieces.charAt(1) == 'k' || Pieces.charAt(1) == 'q') { // King / Queen - for (int i = 0; i < scanXKQ.length; i++) { - for (int j = 1; j <= Visibility; j += 1) { - try { - - if (Chessboard.board[y - (scanYKQ[i] * j)][x + (scanXKQ[i] * j)] == null) { - posiblemove[y - (scanYKQ[i] * j)][x + (scanXKQ[i] * j)] = "Posible"; - } - - else if (Chessboard.board[y - (scanYKQ[i] * j)][x + (scanXKQ[i] * j)].charAt(0) == 'B') { - posiblemove[y - (scanYKQ[i] * j)][x + (scanXKQ[i] * j)] = "Posible"; - break; - - } else if (Pieces.charAt(1) == 'k' || Pieces.charAt(1) == 'q') { - - if (Chessboard.board[y - (scanYKQ[i] * j)][x + (scanXKQ[i] * j)].charAt(0) == 'W') { - - break; - } - } - } catch (ArrayIndexOutOfBoundsException e) { - continue; - } - - } - } - } else if (Pieces.charAt(1) == 'p') { // Pawn - for (int i = 0; i < scanXpawn.length; i++) { - for (int j = 1; j <= Visibility; j += 1) { - try { - - if (Chessboard.board[y - (scanYpawn[i] * j)][x + (scanXpawn[i] * j)] == null) { - if (scanXpawn[i] == 0) { - if (y == 6) { - posiblemove[y - (scanYpawn[i] * 2 * j)][x + (scanXpawn[i] * j)] = "Posible"; - } - posiblemove[y - (scanYpawn[i] * j)][x + (scanXpawn[i] * j)] = "Posible"; - } - } else if (Chessboard.board[y - (scanYpawn[i] * j)][x + (scanXpawn[i] * j)] - .charAt(0) == 'B') { - if (scanXpawn[i] != 0) { - posiblemove[y - (scanYpawn[i] * j)][x + (scanXpawn[i] * j)] = "Posible"; - break; - } - } - } catch (ArrayIndexOutOfBoundsException e) { - continue; - } - - } - } - } - - else if (Pieces.charAt(1) == 'b') { - for (int i = 0; i < scanXbis.length; i++) { - for (int j = 1; j <= Visibility; j += 1) { - try { - - if (Chessboard.board[y - (scanYbis[i] * j)][x + (scanXbis[i] * j)] == null) { - posiblemove[y - (scanYbis[i] * j)][x + (scanXbis[i] * j)] = "Posible"; - } - - else if (Chessboard.board[y - (scanYbis[i] * j)][x + (scanXbis[i] * j)].charAt(0) == 'B') { - - posiblemove[y - (scanYbis[i] * j)][x + (scanXbis[i] * j)] = "Posible"; - break; - - } else if (Chessboard.board[y - (scanYbis[i] * j)][x + (scanXbis[i] * j)] - .charAt(0) == 'W') { - - break; - } - } catch (ArrayIndexOutOfBoundsException e) { - continue; - } - - } - } - } else if (Pieces.charAt(1) == 'r') { - for (int i = 0; i < scanXrook.length; i++) { - for (int j = 1; j <= Visibility; j += 1) { - try { - - if (Chessboard.board[y - (scanYrook[i] * j)][x + (scanXrook[i] * j)] == null) { - posiblemove[y - (scanYrook[i] * j)][x + (scanXrook[i] * j)] = "Posible"; - } - - else if (Chessboard.board[y - (scanYrook[i] * j)][x + (scanXrook[i] * j)] - .charAt(0) == 'B') { - - posiblemove[y - (scanYrook[i] * j)][x + (scanXrook[i] * j)] = "Posible"; - break; - - } else if (Chessboard.board[y - (scanYrook[i] * j)][x + (scanXrook[i] * j)] - .charAt(0) == 'W') { - - break; - } - } catch (ArrayIndexOutOfBoundsException e) { - continue; - } - - } - } - } else if (Pieces.charAt(1) == 'n') { - System.out.println("pass"); - for (int i = 0; i < scanXknight.length; i++) { - - try { - - if (Chessboard.board[y + (scanYknight[i])][x + (scanXknight[i])] == null) { - posiblemove[y + (scanYknight[i])][x + (scanXknight[i])] = "Posible"; - } - - else if (Chessboard.board[y + (scanYknight[i])][x + (scanXknight[i])] - .charAt(0) == 'B') { - - posiblemove[y + (scanYknight[i])][x + (scanXknight[i])] = "Posible"; - continue; - - } else if (Chessboard.board[y + (scanYknight[i])][x + (scanXknight[i])] - .charAt(0) == 'W') { - - continue; - } - } catch (ArrayIndexOutOfBoundsException e) { - continue; - } - - } - - } - } - } - - public void checkking(int y, int x) { - posiblemove[y][x] = "Check"; - } - - public static void promotion(int y, int x, boolean color, int yprev, int xprev) { - if (color == true && y == 0) { - - Chessboard.board[yprev][xprev] = null; - Chessboard.board[y][x] = "Wqueen"; - } else if (color == false && y == 7) { - Chessboard.board[yprev][xprev] = null; - Chessboard.board[y][x] = "Bqueen"; - } - } - -} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6e6d8e --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Chess Game + +A local two-player chess game built with Java Swing. The board shows legal moves, checks, move history, and a clock for each player. + +![Chess Game preview](docs/preview.png) + +## Run + +Install JDK 17 or newer. On macOS or Linux, run: + +```sh +./run.sh +``` + +The script creates `dist/ChessGame.jar`. You can launch the built game later with `java -jar dist/ChessGame.jar`. + +To run the headless chess-rule tests: + +```sh +./test.sh +``` + +## Play + +Choose a clock preset, then click a piece and a highlighted destination. White moves first. When a pawn reaches the far rank, choose its promotion piece. The presets show minutes plus seconds added after each move. Use **New game** to reset the board and clocks; **Agree draw** and **Resign** end the current game. A player loses when their clock runs out. + +You can also use the arrow keys to focus a square, Enter or Space to select it, and Escape to clear the selection. + +The rules engine handles legal moves, check, checkmate, stalemate, castling, en passant, promotion, and common insufficient-material draws. This is a same-computer game; it does not include a computer opponent or online play. + +Draws by repetition and the fifty-move rule are not yet detected automatically. + +## Project structure + +- `ChessGame.java`: chess state and rules, independent of Swing. +- `ChessApp.java`: desktop interface, clocks, board rendering, and controls. +- `ChessGameTest.java`: headless rule checks. +- `W*.png` and `B*.png`: piece artwork from the original project. The knight files retain their original `night` spelling. + +The earlier prototype files were replaced by the standalone rules engine and interface. The project has no external dependencies. diff --git a/TestGraphics.java b/TestGraphics.java deleted file mode 100644 index 831dff8..0000000 --- a/TestGraphics.java +++ /dev/null @@ -1,56 +0,0 @@ -import javax.swing.*; -import java.awt.*; - -public class TestGraphics extends JFrame { - public static void main(String[] args) { - - JLabel whitetime = new JLabel("00:00"); - JLabel blacktime = new JLabel("00:00"); - JFrame settime = new JFrame("Setting time"); - - Chessclock time = new Chessclock(whitetime, blacktime); - settime.add(time); - settime.setVisible(true); - settime.setSize(300, 300); - - JLabel status = new JLabel(); - JFrame frame = new JFrame("Test1"); - JPanel sidetimer = new JPanel(); - sidetimer.setSize(new Dimension(200, 200)); - - JButton draw2 = new JButton("Draw"); - JButton draw1 = new JButton("Draw"); - JButton resign1 = new JButton("Resgin"); - JButton resign2 = new JButton("Resign"); - JPanel center = new JPanel(); - center.setLayout(new GridLayout(2, 2)); - center.add(resign2); - center.add(draw2); - center.add(resign1); - center.add(draw1); - - whitetime.setHorizontalAlignment(SwingConstants.CENTER); - blacktime.setHorizontalAlignment(SwingConstants.CENTER); - sidetimer.setLayout(new BorderLayout()); - sidetimer.add(blacktime, BorderLayout.NORTH); - sidetimer.add(center, BorderLayout.CENTER); - sidetimer.add(whitetime, BorderLayout.SOUTH); - - Chessboard game = new Chessboard(); - frame.add(game, BorderLayout.CENTER); - game.setSize(new Dimension(800, 800)); - frame.add(sidetimer, BorderLayout.EAST); - MouseMotion movement = new MouseMotion(game, status, resign1, resign2, draw1, draw2); - - frame.add(status, BorderLayout.SOUTH); - frame.addMouseListener(movement); - frame.addMouseMotionListener(movement); - - frame.setBackground(Color.decode("#eeeed2")); - frame.setVisible(true); - frame.setSize(963, 830); - frame.setLocationRelativeTo(null); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - - } -} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..31f48c6 --- /dev/null +++ b/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" +mkdir -p build/classes dist +javac -encoding UTF-8 -d build/classes ChessGame.java ChessApp.java +cp ./*.png build/classes/ +jar --create --file dist/ChessGame.jar --main-class ChessApp -C build/classes . +echo "Built dist/ChessGame.jar" diff --git a/docs/preview.png b/docs/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..b75dd9760aac478a277953700a258ec661372d81 GIT binary patch literal 76682 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