diff --git a/README.md b/README.md new file mode 100644 index 0000000..837d79e --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Checkers JavaFX + +Dwuosobowa gra w warcaby napisana w Javie z użyciem JavaFX. + +## Zasady gry + +- Gra toczy się na szachownicy 8×8. +- **Gracz 1** (białe pionki) zaczyna jako pierwszy i porusza się w górę planszy. +- **Gracz 2** (czerwone pionki) porusza się w dół planszy. +- Pionki poruszają się o jedno pole po skosie na wolne, ciemne pole. +- Bicie jest **obowiązkowe** — jeśli istnieje możliwość bicia, trzeba z niej skorzystać. +- Bicie wielokrotne: jeśli po biciu pionek może bić kolejny, musi kontynuować serię. +- Pionek, który dotrze do ostatniego rzędu po stronie przeciwnika, staje się **damką** — może poruszać się po skosie w obu kierunkach. +- Gracz, który zbije wszystkie pionki przeciwnika, wygrywa. + +## Sterowanie + +Pionki przeciąga się myszką (drag & drop) na docelowe pole. +Jeśli ruch jest niedozwolony, pionek wraca na swoje miejsce. + +## Wymagania + +- **Java 18+** +- **Maven 3.8+** + +## Uruchomienie + +```bash +mvn clean javafx:run +``` + +## Struktura projektu + +``` +src/main/ +├── java/com/example/checkers/ +│ ├── CheckersApp.java — punkt wejścia, logika gry i planszy +│ ├── Controller.java — kontroler FXML (liczniki, timer, komunikat o zwycięstwie) +│ ├── Piece.java — pionek z obsługą drag & drop +│ ├── Tile.java — pole planszy +│ ├── PieceType.java — typ pionka (RED, WHITE, REDKING, WHITEKING) +│ ├── MoveType.java — wynik ruchu (NONE, NORMAL, CAPTURE) +│ ├── MoveResult.java — wrapper wyniku ruchu wraz z bitym pionkiem +│ └── StopWatch.java — stoper odmierzający czas od ostatniego ruchu +└── resources/com/example/checkers/ + ├── Board.fxml — układ głównego okna + ├── RedPiece.fxml — wygląd czerwonego pionka + ├── WhitePiece.fxml — wygląd białego pionka + ├── RedKingPiece.fxml — wygląd czerwonej damki + └── WhiteKingPiece.fxml — wygląd białej damki +``` + +## Technologie + +| Technologia | Wersja | +|---|---| +| Java | 18 | +| JavaFX | 18-ea+6 | +| Maven | 3.8.1 | +| JUnit Jupiter | 5.8.1 | diff --git a/src/main/java/com/example/checkers/CheckersApp.java b/src/main/java/com/example/checkers/CheckersApp.java index 7ac3ebb..34eb0a8 100644 --- a/src/main/java/com/example/checkers/CheckersApp.java +++ b/src/main/java/com/example/checkers/CheckersApp.java @@ -1,19 +1,15 @@ package com.example.checkers; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; import javafx.application.Application; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; import javafx.scene.Group; import javafx.scene.Parent; import javafx.scene.Scene; -import javafx.scene.control.Label; +import javafx.scene.effect.DropShadow; import javafx.scene.layout.Pane; -import javafx.scene.shape.Ellipse; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; import javafx.stage.Stage; -import javafx.util.Duration; import java.io.IOException; import java.util.Objects; @@ -23,57 +19,69 @@ public class CheckersApp extends Application { public static final int TILE_SIZE = 100; - public static final int WIDTH = 8; + public static final int WIDTH = 8; public static final int HEIGHT = 8; - private int turn; - private final Tile[][] board = new Tile[WIDTH][HEIGHT]; - private final Group tileGroup = new Group(); + /** Pixel offset so the board has breathing room from the window edge. + * Must be >= piece radiusX (36 px) so column-0 pieces are never clipped. */ + private static final int BOARD_OFFSET = 40; + + private int turn = 0; + private final Tile[][] board = new Tile[WIDTH][HEIGHT]; + private final Group tileGroup = new Group(); private final Group pieceGroup = new Group(); private Piece mustStrike = null; - private Pane root; - Controller controller = new Controller(); - StopWatch stopwatch = new StopWatch(); - - Timeline timeline = new Timeline( - new KeyFrame(Duration.seconds(0), - new EventHandler<>() { - @Override - public void handle(ActionEvent actionEvent) { - ((Label) root.getChildren().get(4)).setText(Integer.toString(controller.redCounter)); - ((Label) root.getChildren().get(5)).setText(Integer.toString(controller.whiteCounter)); - ((Label) root.getChildren().get(7)).setText(stopwatch.text.getValue()); - ((Label) root.getChildren().get(8)).setText(controller.playerWon()); - if (!Objects.equals(controller.playerWon(), "")) { - stopwatch.timeline.stop(); - } - } - } - ), - new KeyFrame(Duration.millis(1)) - ); + private Controller controller; + private final StopWatch stopwatch = new StopWatch(); + + @Override + public void start(Stage stage) throws IOException { + Scene scene = new Scene(createContent()); + stage.setTitle("Warcaby"); + stage.setResizable(false); + stage.setScene(scene); + stage.show(); + stopwatch.start(); + } private Parent createContent() throws IOException { - root = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("Board.fxml"))); - root.getChildren().addAll(tileGroup, pieceGroup); - turn = 0; + FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(getClass().getResource("Board.fxml"))); + Pane root = loader.load(); + controller = loader.getController(); + controller.bindTimer(stopwatch.textProperty()); + + // Offset the board groups so tiles are inset from the window edge + tileGroup.setTranslateX(BOARD_OFFSET); + tileGroup.setTranslateY(BOARD_OFFSET); + pieceGroup.setTranslateX(BOARD_OFFSET); + pieceGroup.setTranslateY(BOARD_OFFSET); + + // Soft drop shadow on the board surface + tileGroup.setEffect(new DropShadow(18, 3, 3, Color.web("#00000088"))); + + // Thin gold border drawn on top of tiles, below pieces + Rectangle boardBorder = new Rectangle( + BOARD_OFFSET, BOARD_OFFSET, + WIDTH * TILE_SIZE, HEIGHT * TILE_SIZE); + boardBorder.setFill(Color.TRANSPARENT); + boardBorder.setStroke(Color.web("#8B6914")); + boardBorder.setStrokeWidth(2); + boardBorder.setMouseTransparent(true); + + root.getChildren().add(tileGroup); + root.getChildren().add(boardBorder); + root.getChildren().add(pieceGroup); + for (int y = 0; y < HEIGHT; y++) { for (int x = 0; x < WIDTH; x++) { Tile tile = new Tile((x + y) % 2 == 0, x, y); board[x][y] = tile; - tileGroup.getChildren().add(tile); Piece piece = null; - - if (y <= 2 && (x + y) % 2 != 0) { - piece = makePiece(PieceType.RED, x, y); - } - - if (y >= 5 && (x + y) % 2 != 0) { - piece = makePiece(PieceType.WHITE, x, y); - } + if (y <= 2 && (x + y) % 2 != 0) piece = makePiece(RED, x, y); + if (y >= 5 && (x + y) % 2 != 0) piece = makePiece(WHITE, x, y); if (piece != null) { tile.setPiece(piece); @@ -84,226 +92,154 @@ private Parent createContent() throws IOException { return root; } + // ───────────────────────────────────────────────────────────────────────── + // Move validation + // ───────────────────────────────────────────────────────────────────────── + private MoveResult tryMove(Piece piece, int newX, int newY) { - if (board[newX][newY].hasPiece() || (newX + newY) % 2 == 0 || ((piece.getType() == WHITE || piece.getType() == WHITEKING) && turn % 2 == 1) || - ((piece.getType() == RED || piece.getType() == REDKING) && turn % 2 == 0)) { + if (board[newX][newY].hasPiece() || (newX + newY) % 2 == 0 || isWrongTurn(piece.getType())) { return new MoveResult(MoveType.NONE); } + int x0 = toBoard(piece.getOldX()); int y0 = toBoard(piece.getOldY()); + PieceType type = piece.getType(); + int dx = newX - x0; + int dy = newY - y0; + + mustStrike = checkIfAnyPieceCanCapture(type); + + boolean isNormalMove = Math.abs(dx) == 1 + && (dy == type.moveDir || dy == type.secondaryDir) + && mustStrike == null; - mustStrike = checkIfAnyCheckerIsStrikable(piece.getType()); - if (Math.abs(newX - x0) == 1 && (newY - y0 == piece.getType().moveDir || newY - y0 == piece.getType().isKing) && mustStrike == null) { //sprawdzanie czy ktoś nie chce sie ruszyc o wiecej niz 1 pole + boolean isCaptureMove = Math.abs(dx) == 2 + && (dy == type.moveDir * 2 || dy == type.secondaryDir * 2); + + if (isNormalMove) { turn++; stopwatch.reset(); return new MoveResult(MoveType.NORMAL); - } else if ((Math.abs(newX - x0) == 2 && (newY - y0 == piece.getType().moveDir * 2 || newY - y0 == piece.getType().isKing * 2))) { - int x1 = x0 + (newX - x0) / 2; - int y1 = y0 + (newY - y0) / 2; - if (board[x1][y1].hasPiece() && (((board[x1][y1].getPiece().getType() == WHITE || board[x1][y1].getPiece().getType() == WHITEKING) && - (piece.getType() == RED || piece.getType() == REDKING)) || - ((board[x1][y1].getPiece().getType() == RED || board[x1][y1].getPiece().getType() == REDKING) && - (piece.getType() == WHITE || piece.getType() == WHITEKING)))) { - boolean canAttackAgain = canStrikeMore(piece.getType(), newX, newY); - if (canAttackAgain) { + } + + if (isCaptureMove) { + int midX = x0 + dx / 2; + int midY = y0 + dy / 2; + if (board[midX][midY].hasPiece() && isOpponent(type, board[midX][midY].getPiece().getType())) { + if (canCaptureAgain(type, newX, newY)) { mustStrike = piece; - return new MoveResult(MoveType.KILL, board[x1][y1].getPiece()); } else { turn++; stopwatch.reset(); mustStrike = null; - return new MoveResult(MoveType.KILL, board[x1][y1].getPiece()); } - + return new MoveResult(MoveType.CAPTURE, board[midX][midY].getPiece()); } } + return new MoveResult(MoveType.NONE); } - boolean canStrikeMore(PieceType type, int x, int y) { - if ((type == PieceType.WHITE && y < 2) || (type == PieceType.RED && y > 5)) - return false; - boolean isStrikable = false; - if (type == PieceType.WHITE) { - if (x >= 2 && x <= 7) { - if (board[x - 1][y - 1].hasPiece() && (board[x - 1][y - 1].getPiece().getType() == PieceType.RED || - board[x - 1][y - 1].getPiece().getType() == REDKING) && - !board[x - 2][y - 2].hasPiece()) - isStrikable = true; - } - if (x >= 0 && x < 6) { - if (board[x + 1][y - 1].hasPiece() && (board[x + 1][y - 1].getPiece().getType() == PieceType.RED || - board[x + 1][y - 1].getPiece().getType() == REDKING) && - !board[x + 2][y - 2].hasPiece()) { - isStrikable = true; - } - } - } else if (type == PieceType.RED) { - if (x > 1 && x < 7) { - if (board[x - 1][y + 1].hasPiece() && (board[x - 1][y + 1].getPiece().getType() == PieceType.WHITE || - board[x - 1][y + 1].getPiece().getType() == WHITEKING) && - !board[x - 2][y + 2].hasPiece()) - isStrikable = true; - } - if (x >= 0 && x < 6) { - if (board[x + 1][y + 1].hasPiece() && (board[x + 1][y + 1].getPiece().getType() == PieceType.WHITE || - board[x + 1][y + 1].getPiece().getType() == WHITEKING) && - !board[x + 2][y + 2].hasPiece()) - isStrikable = true; - } - } else if (type == WHITEKING) { - if (x >= 2 && x <= 7 && y >= 2) { - if (board[x - 1][y - 1].hasPiece() && (board[x - 1][y - 1].getPiece().getType() == PieceType.RED || - board[x - 1][y - 1].getPiece().getType() == REDKING) && - !board[x - 2][y - 2].hasPiece()) - isStrikable = true; - } - if (x >= 0 && x < 6 && y >= 2) { - if (board[x + 1][y - 1].hasPiece() && (board[x + 1][y - 1].getPiece().getType() == PieceType.RED || - board[x + 1][y - 1].getPiece().getType() == REDKING) && - !board[x + 2][y - 2].hasPiece()) { - isStrikable = true; - } - } - if (x > 1 && x < 7 && y < 6) { - if (board[x - 1][y + 1].hasPiece() && (board[x - 1][y + 1].getPiece().getType() == PieceType.RED || - board[x - 1][y + 1].getPiece().getType() == REDKING) && - !board[x - 2][y + 2].hasPiece()) - isStrikable = true; - } - if (x >= 0 && x < 6 && y < 6) { - if (board[x + 1][y + 1].hasPiece() && (board[x + 1][y + 1].getPiece().getType() == PieceType.RED || - board[x + 1][y + 1].getPiece().getType() == REDKING) && - !board[x + 2][y + 2].hasPiece()) - isStrikable = true; - } - } else if (type == REDKING) { - if (x >= 2 && x <= 7 && y >= 2) { - if (board[x - 1][y - 1].hasPiece() && (board[x - 1][y - 1].getPiece().getType() == WHITE || - board[x - 1][y - 1].getPiece().getType() == WHITEKING) && - !board[x - 2][y - 2].hasPiece()) - isStrikable = true; - } - if (x >= 0 && x < 6 && y >= 2) { - if (board[x + 1][y - 1].hasPiece() && (board[x + 1][y - 1].getPiece().getType() == PieceType.WHITE || - board[x + 1][y - 1].getPiece().getType() == WHITEKING) && - !board[x + 2][y - 2].hasPiece()) { - isStrikable = true; - } - } - if (x > 1 && x < 7 && y < 6) { - if (board[x - 1][y + 1].hasPiece() && (board[x - 1][y + 1].getPiece().getType() == PieceType.WHITE || - board[x - 1][y + 1].getPiece().getType() == WHITEKING) && - !board[x - 2][y + 2].hasPiece()) - isStrikable = true; - } - if (x >= 0 && x < 6 && y < 6) { - if (board[x + 1][y + 1].hasPiece() && (board[x + 1][y + 1].getPiece().getType() == PieceType.WHITE || - board[x + 1][y + 1].getPiece().getType() == WHITEKING) && - !board[x + 2][y + 2].hasPiece()) - isStrikable = true; - } - } - return isStrikable; + private boolean isWrongTurn(PieceType type) { + boolean isRed = type == RED || type == REDKING; + return isRed == (turn % 2 == 0); // even turns: white; odd turns: red } - private int toBoard(double pixel) { - return (int) (pixel + TILE_SIZE / 2) / TILE_SIZE; + private boolean isOpponent(PieceType mover, PieceType other) { + boolean moverIsRed = mover == RED || mover == REDKING; + boolean otherIsRed = other == RED || other == REDKING; + return moverIsRed != otherIsRed; } - @Override - public void start(Stage stage) throws IOException { - Scene scene = new Scene(createContent()); - stage.setTitle("Checkers"); - stage.setScene(scene); - stage.show(); - timeline.setCycleCount(Timeline.INDEFINITE); - timeline.setAutoReverse(false); - timeline.play(); + private boolean canCaptureInDirection(PieceType type, int x, int y, int dx, int dy) { + int midX = x + dx, midY = y + dy; + int landX = x + 2 * dx, landY = y + 2 * dy; + if (landX < 0 || landX >= WIDTH || landY < 0 || landY >= HEIGHT) return false; + return board[midX][midY].hasPiece() + && isOpponent(type, board[midX][midY].getPiece().getType()) + && !board[landX][landY].hasPiece(); } + boolean canCaptureAgain(PieceType type, int x, int y) { + int fwd = type.moveDir; + if (canCaptureInDirection(type, x, y, -1, fwd)) return true; + if (canCaptureInDirection(type, x, y, 1, fwd)) return true; + if (type.isKing()) { + int bwd = type.secondaryDir; + if (canCaptureInDirection(type, x, y, -1, bwd)) return true; + if (canCaptureInDirection(type, x, y, 1, bwd)) return true; + } + return false; + } + + private Piece checkIfAnyPieceCanCapture(PieceType type) { + for (int y = 0; y < HEIGHT; y++) + for (int x = 0; x < WIDTH; x++) + if (board[x][y].hasPiece() && board[x][y].getPiece().getType() == type + && canCaptureAgain(type, x, y)) + return board[x][y].getPiece(); + return null; + } + + // ───────────────────────────────────────────────────────────────────────── + // Piece creation & move execution + // ───────────────────────────────────────────────────────────────────────── + private Piece makePiece(PieceType type, int x, int y) throws IOException { Piece piece = new Piece(type, x, y); piece.setOnMouseReleased(e -> { + piece.resetScale(); - - mustStrike = checkIfAnyCheckerIsStrikable(piece.getType()); - int newX = toBoard(piece.getLayoutX()); //moving mouse changes layout + int newX = toBoard(piece.getLayoutX()); int newY = toBoard(piece.getLayoutY()); - MoveResult result = tryMove(piece, newX, newY); int x0 = toBoard(piece.getOldX()); int y0 = toBoard(piece.getOldY()); switch (result.getType()) { - case NONE -> piece.abortMove(); + case NORMAL -> { - try { - changeToKing(piece, newY); - } catch (IOException ex) { - ex.printStackTrace(); - } + promoteToKingIfNeeded(piece, newY); piece.move(newX, newY); board[x0][y0].setPiece(null); board[newX][newY].setPiece(piece); } - case KILL -> { - try { - changeToKing(piece, newY); - } catch (IOException ex) { - ex.printStackTrace(); - } + + case CAPTURE -> { + promoteToKingIfNeeded(piece, newY); piece.move(newX, newY); - if (board[x0][y0].getPiece().getType() == RED || board[x0][y0].getPiece().getType() == REDKING) { - controller.decrementWhite(); - } else { - controller.decrementRed(); - } board[x0][y0].setPiece(null); board[newX][newY].setPiece(piece); - Piece otherPiece = result.getPiece(); - board[toBoard(otherPiece.getOldX())][toBoard(otherPiece.getOldY())].setPiece(null); - pieceGroup.getChildren().remove(otherPiece); + Piece captured = result.getCapturedPiece(); + board[toBoard(captured.getOldX())][toBoard(captured.getOldY())].setPiece(null); + pieceGroup.getChildren().remove(captured); + + boolean capturedIsRed = captured.getType() == RED || captured.getType() == REDKING; + if (capturedIsRed) controller.decrementRed(); + else controller.decrementWhite(); + + if (controller.isGameOver()) stopwatch.stop(); } } }); return piece; } - private Piece checkIfAnyCheckerIsStrikable(PieceType type) { - for (int y = 0; y < HEIGHT; y++) { - for (int x = 0; x < WIDTH; x++) { - if (board[x][y].hasPiece() && board[x][y].getPiece().getType() == type && canStrikeMore(type, x, y)) { - return board[x][y].getPiece(); - } - } + private void promoteToKingIfNeeded(Piece piece, int y) { + try { + if (piece.getType() == RED && y == HEIGHT - 1) + piece.promoteToKing(REDKING, getClass().getResource("RedKingPiece.fxml")); + else if (piece.getType() == WHITE && y == 0) + piece.promoteToKing(WHITEKING, getClass().getResource("WhiteKingPiece.fxml")); + } catch (IOException ex) { + ex.printStackTrace(); } - return null; } - private void changeToKing(Piece piece, int y) throws IOException { - if (piece.getType() == PieceType.RED && y == 7) { - piece.setType(REDKING); - for (int i = 0; i < pieceGroup.getChildren().size(); i++) { - if (pieceGroup.getChildren().get(i) == piece) { - Ellipse bp = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("RedKingPiece.fxml"))); - piece.getChildren().set(0, bp); - return; - } - - } - } else if (piece.getType() == PieceType.WHITE && y == 0) { - piece.setType(PieceType.WHITEKING); - for (int i = 0; i < pieceGroup.getChildren().size(); i++) { - if (pieceGroup.getChildren().get(i) == piece) { - Ellipse bp = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("WhiteKingPiece.fxml"))); - piece.getChildren().set(0, bp); - return; - } - } - } + private int toBoard(double pixel) { + return (int) (pixel + TILE_SIZE / 2) / TILE_SIZE; } public static void main(String[] args) { diff --git a/src/main/java/com/example/checkers/Controller.java b/src/main/java/com/example/checkers/Controller.java index 8b1fcc4..8c4b0c5 100644 --- a/src/main/java/com/example/checkers/Controller.java +++ b/src/main/java/com/example/checkers/Controller.java @@ -1,5 +1,6 @@ package com.example.checkers; +import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Label; @@ -7,32 +8,36 @@ import java.net.URL; import java.util.ResourceBundle; -public class Controller implements Initializable{ - @FXML - public Label whiteLabel = new Label(); - @FXML - public Label redLabel = new Label(); +public class Controller implements Initializable { + @FXML private Label redLabel; + @FXML private Label whiteLabel; + @FXML private Label timerLabel; + @FXML private Label winLabel; + private int redCount = 12; + private int whiteCount = 12; - public int redCounter=12; - public int whiteCounter=12; + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + redLabel.setText("12"); + whiteLabel.setText("12"); + } + public void bindTimer(StringProperty timerText) { + timerLabel.textProperty().bind(timerText); + } - public void decrementRed(){ - redCounter--; + public void decrementRed() { + redLabel.setText(String.valueOf(--redCount)); + if (redCount == 0) winLabel.setText("Gracz 2 wygrał!"); } - public void decrementWhite(){whiteCounter--;} - @Override - public void initialize(URL url, ResourceBundle resourceBundle) { - whiteLabel.setText(Integer.toString(whiteCounter=12)); - redLabel.setText(Integer.toString(redCounter=12)); + + public void decrementWhite() { + whiteLabel.setText(String.valueOf(--whiteCount)); + if (whiteCount == 0) winLabel.setText("Gracz 1 wygrał!"); } - public String playerWon() { - if (redCounter == 0) { - return "Gracz 2 wygral"; - }else if (whiteCounter ==0){ - return "Gracz 1 wygral"; - } - else return ""; + + public boolean isGameOver() { + return redCount == 0 || whiteCount == 0; } } diff --git a/src/main/java/com/example/checkers/MoveResult.java b/src/main/java/com/example/checkers/MoveResult.java index eab42a9..a2b6c0c 100644 --- a/src/main/java/com/example/checkers/MoveResult.java +++ b/src/main/java/com/example/checkers/MoveResult.java @@ -2,24 +2,22 @@ public class MoveResult { private final MoveType type; - private final Piece piece; - - public MoveType getType() { - return type; - } - - - public Piece getPiece() { - return piece; - } + private final Piece capturedPiece; public MoveResult(MoveType type) { this(type, null); } - public MoveResult(MoveType type, Piece piece) { + public MoveResult(MoveType type, Piece capturedPiece) { this.type = type; - this.piece = piece; + this.capturedPiece = capturedPiece; + } + + public MoveType getType() { + return type; } + public Piece getCapturedPiece() { + return capturedPiece; + } } diff --git a/src/main/java/com/example/checkers/MoveType.java b/src/main/java/com/example/checkers/MoveType.java index ebdef8f..989720f 100644 --- a/src/main/java/com/example/checkers/MoveType.java +++ b/src/main/java/com/example/checkers/MoveType.java @@ -1,5 +1,5 @@ package com.example.checkers; public enum MoveType { - NONE, NORMAL, KILL + NONE, NORMAL, CAPTURE } diff --git a/src/main/java/com/example/checkers/Piece.java b/src/main/java/com/example/checkers/Piece.java index 67c0f89..b13c075 100644 --- a/src/main/java/com/example/checkers/Piece.java +++ b/src/main/java/com/example/checkers/Piece.java @@ -1,10 +1,13 @@ package com.example.checkers; import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.effect.DropShadow; import javafx.scene.layout.StackPane; -import javafx.scene.shape.Ellipse; +import javafx.scene.paint.Color; import java.io.IOException; +import java.net.URL; import java.util.Objects; import static com.example.checkers.CheckersApp.TILE_SIZE; @@ -14,37 +17,49 @@ public class Piece extends StackPane { private double mouseX, mouseY; private double oldX, oldY; - public double getOldX() { - return oldX; - } + public Piece(PieceType type, int x, int y) throws IOException { + this.type = type; - public double getOldY() { - return oldY; - } + // Force the StackPane to exactly fill its tile so the visual is + // centred inside the square (otherwise it shrinks to the inner + // Group's bounds and renders in the top-left corner). + setMinSize(TILE_SIZE, TILE_SIZE); + setPrefSize(TILE_SIZE, TILE_SIZE); + setMaxSize(TILE_SIZE, TILE_SIZE); + setPickOnBounds(false); // hover/click only on visible pixels - PieceType getType() { - return type; - } + move(x, y); - public void setType(PieceType type) { - this.type = type; - } + String fxmlName = switch (type) { + case RED -> "RedPiece.fxml"; + case WHITE -> "WhitePiece.fxml"; + case REDKING -> "RedKingPiece.fxml"; + case WHITEKING -> "WhiteKingPiece.fxml"; + }; - public Piece(PieceType type, int x, int y) throws IOException { - this.type = type; - move(x, y); - Ellipse bg; - switch (type) { - case RED -> bg = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("RedPiece.fxml"))); - case WHITE -> bg = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("WhitePiece.fxml"))); - default -> bg = null; - } - - getChildren().add(bg); + Node visual = FXMLLoader.load(Objects.requireNonNull(getClass().getResource(fxmlName))); + getChildren().add(visual); + + // Hover glow + setOnMouseEntered(e -> { + if (getScaleX() == 1.0) + setEffect(new DropShadow(14, Color.web("#ffffff55"))); + }); + setOnMouseExited(e -> { + if (getScaleX() == 1.0) + setEffect(null); + }); + + // Scale-up + shadow on press setOnMousePressed(e -> { mouseX = e.getSceneX(); mouseY = e.getSceneY(); + setScaleX(1.13); + setScaleY(1.13); + setEffect(new DropShadow(20, Color.web("#00000099"))); + toFront(); }); + setOnMouseDragged(e -> relocate(e.getSceneX() - mouseX + oldX, e.getSceneY() - mouseY + oldY)); } @@ -56,5 +71,23 @@ public void move(int x, int y) { public void abortMove() { relocate(oldX, oldY); + resetScale(); } + + public void resetScale() { + setScaleX(1.0); + setScaleY(1.0); + setEffect(null); + } + + public void promoteToKing(PieceType kingType, URL fxmlUrl) throws IOException { + this.type = kingType; + Node kingVisual = FXMLLoader.load(Objects.requireNonNull(fxmlUrl)); + getChildren().set(0, kingVisual); + } + + public PieceType getType() { return type; } + public void setType(PieceType type) { this.type = type; } + public double getOldX() { return oldX; } + public double getOldY() { return oldY; } } diff --git a/src/main/java/com/example/checkers/PieceType.java b/src/main/java/com/example/checkers/PieceType.java index 9aa42c5..b52a636 100644 --- a/src/main/java/com/example/checkers/PieceType.java +++ b/src/main/java/com/example/checkers/PieceType.java @@ -1,13 +1,20 @@ package com.example.checkers; public enum PieceType { - RED(1, 0), WHITE(-1, 0), REDKING(1,-1), WHITEKING(-1, 1), ; + RED(1, 0), + WHITE(-1, 0), + REDKING(1, -1), + WHITEKING(-1, 1); + final int moveDir; - public int isKing; + final int secondaryDir; // reverse diagonal direction for kings; 0 for regular pieces - PieceType(int moveDIR, int isKing){ - this.moveDir = moveDIR; - this.isKing = isKing; + PieceType(int moveDir, int secondaryDir) { + this.moveDir = moveDir; + this.secondaryDir = secondaryDir; } + public boolean isKing() { + return secondaryDir != 0; + } } diff --git a/src/main/java/com/example/checkers/StopWatch.java b/src/main/java/com/example/checkers/StopWatch.java index 07043af..e3b6b50 100644 --- a/src/main/java/com/example/checkers/StopWatch.java +++ b/src/main/java/com/example/checkers/StopWatch.java @@ -4,42 +4,46 @@ import javafx.animation.Timeline; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import javafx.scene.layout.StackPane; import javafx.util.Duration; -public class StopWatch extends StackPane { - StringProperty text = new SimpleStringProperty(); - Timeline timeline; - int mins = 0, secs = 0, millis = 0; - - - void change(StringProperty text) { - if (millis == 1000) { - secs++; - millis = 0; - } - if (secs == 60) { - mins++; - secs = 0; - } - text.set(((((mins / 10) == 0) ? "0" : "") + mins + ":" - + (((secs / 10) == 0) ? "0" : "") + secs + ":" - + (((millis / 10) == 0) ? "00" : (((millis / 100) == 0) ? "0" : "")) + millis++)); - } +public class StopWatch { + private final StringProperty text = new SimpleStringProperty("00:00:000"); + private final Timeline timeline; + private int mins, secs, millis; public StopWatch() { - - timeline = new Timeline(new KeyFrame(Duration.millis(1), event -> change(text))); + timeline = new Timeline(new KeyFrame(Duration.millis(1), e -> tick())); timeline.setCycleCount(Timeline.INDEFINITE); timeline.setAutoReverse(false); + } + + public void start() { timeline.playFromStart(); } - void reset() { + public void stop() { + timeline.stop(); + } + + public void reset() { mins = 0; secs = 0; millis = 0; } + public StringProperty textProperty() { + return text; + } + private void tick() { + if (millis == 1000) { + secs++; + millis = 0; + } + if (secs == 60) { + mins++; + secs = 0; + } + text.set(String.format("%02d:%02d:%03d", mins, secs, millis++)); + } } diff --git a/src/main/java/com/example/checkers/Tile.java b/src/main/java/com/example/checkers/Tile.java index bd05d9e..5c85ebe 100644 --- a/src/main/java/com/example/checkers/Tile.java +++ b/src/main/java/com/example/checkers/Tile.java @@ -8,6 +8,14 @@ public class Tile extends Rectangle { private Piece piece; + public Tile(boolean light, int x, int y) { + setWidth(TILE_SIZE); + setHeight(TILE_SIZE); + relocate(x * TILE_SIZE, y * TILE_SIZE); + // Classic warm wood palette + setFill(light ? Color.valueOf("#F0D9B5") : Color.valueOf("#8B4513")); + } + public boolean hasPiece() { return piece != null; } @@ -19,12 +27,4 @@ public Piece getPiece() { public void setPiece(Piece piece) { this.piece = piece; } - - public Tile(boolean colour, int x, int y) { - setWidth(TILE_SIZE); - setHeight(TILE_SIZE); - relocate(x * TILE_SIZE, y * TILE_SIZE); - - setFill(colour ? Color.valueOf("#feb") : Color.valueOf("#582")); - } } diff --git a/src/main/resources/com/example/checkers/Board.fxml b/src/main/resources/com/example/checkers/Board.fxml index de6d761..e79f0c3 100644 --- a/src/main/resources/com/example/checkers/Board.fxml +++ b/src/main/resources/com/example/checkers/Board.fxml @@ -3,50 +3,126 @@ + + - + + - - - - - - - - - diff --git a/src/main/resources/com/example/checkers/RedKingPiece.fxml b/src/main/resources/com/example/checkers/RedKingPiece.fxml index 8ce2f6f..5bcc06a 100644 --- a/src/main/resources/com/example/checkers/RedKingPiece.fxml +++ b/src/main/resources/com/example/checkers/RedKingPiece.fxml @@ -1,11 +1,47 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + diff --git a/src/main/resources/com/example/checkers/RedPiece.fxml b/src/main/resources/com/example/checkers/RedPiece.fxml index 0286ffb..b78daba 100644 --- a/src/main/resources/com/example/checkers/RedPiece.fxml +++ b/src/main/resources/com/example/checkers/RedPiece.fxml @@ -1,11 +1,39 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + diff --git a/src/main/resources/com/example/checkers/WhiteKingPiece.fxml b/src/main/resources/com/example/checkers/WhiteKingPiece.fxml index d8ae85e..7b06a5a 100644 --- a/src/main/resources/com/example/checkers/WhiteKingPiece.fxml +++ b/src/main/resources/com/example/checkers/WhiteKingPiece.fxml @@ -1,10 +1,47 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + diff --git a/src/main/resources/com/example/checkers/WhitePiece.fxml b/src/main/resources/com/example/checkers/WhitePiece.fxml index 6171602..9e0f952 100644 --- a/src/main/resources/com/example/checkers/WhitePiece.fxml +++ b/src/main/resources/com/example/checkers/WhitePiece.fxml @@ -1,10 +1,39 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + +