diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 552dba0..0000000 Binary files a/.DS_Store and /dev/null differ 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 0000000..b75dd97 Binary files /dev/null and b/docs/preview.png differ diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..3f6f503 --- /dev/null +++ b/run.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" +./build.sh +java -jar dist/ChessGame.jar diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..8b46e41 --- /dev/null +++ b/test.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" +./build.sh +mkdir -p build/tests +javac -encoding UTF-8 -cp build/classes -d build/tests ChessGameTest.java +java -ea -cp build/classes:build/tests ChessGameTest