package chess.view.consolerender; import chess.controller.Command; import chess.controller.CommandExecutor; import chess.controller.CommandSender; import chess.controller.commands.*; import chess.controller.commands.PromoteCommand.PromoteType; import chess.controller.event.GameAdapter; import chess.model.Color; import chess.model.Coordinate; import chess.model.Move; import chess.model.Piece; import java.util.List; import java.util.Objects; import java.util.Scanner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Console renderer. */ public class Console extends GameAdapter implements CommandSender { private final Scanner scanner = new Scanner(System.in); private final CommandExecutor commandExecutor; private final ConsolePieceName consolePieceName = new ConsolePieceName(); private boolean captureInput; private final ExecutorService executor; public Console(CommandExecutor commandExecutor, boolean captureInput) { this.commandExecutor = commandExecutor; this.executor = Executors.newSingleThreadExecutor(); this.captureInput = captureInput; } public Console(CommandExecutor commandExecutor) { this(commandExecutor, true); } /** * Translate a string containing chess coordinates (such as "a1" or "d6") to coordinates. * @param coordinates the string to translate * @return the coordinates of the cell * @throws Exception if the string is not valid */ public Coordinate stringToCoordinate(String coordinates) throws Exception { char xPos = coordinates.charAt(0); char yPos = coordinates.charAt(1); int x; if (xPos >= 'A' && xPos <= 'Z') { x = xPos - 'A'; } else if (xPos >= 'a' && xPos <= 'z') { x = xPos - 'a'; } else { throw new Exception("Invalid input"); } if (!(yPos >= '1' && yPos <= '9')) { throw new Exception("Invalid input"); } int y = Coordinate.VALUE_MAX - 1 - (yPos - '1'); return new Coordinate(x, y); } /** * Open a dialog so the user can, during their turn, move a piece, show move previews, or surrender. */ @Override public void onPlayerTurn(Color color, boolean undone) { if (!captureInput) return; System.out.println(Colors.RED + "Player turn: " + color + Colors.RESET); this.executor.submit(() -> { boolean endTurn; String line = "0"; do { if (!line.isEmpty()) { System.out.println(""" Pick your choice: 1 - Move 2 - Show potential moves 3 - Surrender """); System.out.flush(); } line = scanner.nextLine(); endTurn = switch (line) { case "1" -> playerPickedMove(color); case "2" -> playerPickedShowMoves(color); case "3" -> playerPickedSurrender(color); default -> false; }; } while (!endTurn); }); } private boolean playerPickedSurrender(Color color) { sendSurrender(color); return true; } /** * Ask the user to pick a move * @param color the color of the player * @return true if there has been a move, false otherwise */ public boolean playerPickedMove(Color color) { try { System.out.println("Piece to move, or \"castling\" for a castling"); String answer = scanner.nextLine(); if (answer.equalsIgnoreCase("castling")) { return onAskedCastling(); } Coordinate start = stringToCoordinate(answer); System.out.println("New position: "); Coordinate end = stringToCoordinate(scanner.nextLine()); Command.CommandResult result = sendMove(new Move(start, end)); return switch (Objects.requireNonNull(result)) { case Command.CommandResult.Moved, Command.CommandResult.ActionNeeded -> true; default -> false; }; } catch (Exception e) { System.out.println(e.getMessage()); return false; } } /** * Ask the user to pick a piece, and show its potential moves * @param color * @return */ private boolean playerPickedShowMoves(Color color) { try { System.out.println("Piece to examine: "); Coordinate piece = stringToCoordinate(scanner.nextLine()); List allowedMoves = getPieceAllowedMoves(piece); if (allowedMoves.isEmpty()) { System.out.println("No moves allowed for this piece."); return false; } displayMoves(piece, allowedMoves); return false; } catch (Exception e) { System.out.println(e.getMessage()); return false; } } @Override public void onWin(Color color) { System.out.println(Colors.RED + "Victory of player " + color + Colors.RESET); } @Override public void onKingInCheck() { System.out.println(Colors.RED + "Check!" + Colors.RESET); } @Override public void onKingInMat() { System.out.println(Colors.RED + "Checkmate!" + Colors.RESET); } @Override public void onPatSituation() { System.out.println("Pat! It's a draw!"); } @Override public void onSurrender(Color color) { System.out.println("The " + color + " player has surrendered!"); } @Override public void onGameStart() { System.out.println("Game start!"); onBoardUpdate(); } @Override public void onGameEnd() { System.out.println("Thank you for playing!"); this.commandExecutor.close(); this.executor.shutdown(); } /** * Open the dialog to promote a pawn. * @param pieceCoords the coordinates of the pawn to promote */ @Override public void onPromotePawn(Coordinate pieceCoords) { System.out.println("The pawn on the " + pieceCoords + " coordinates needs to be promoted."); System.out.println("Enter 'B' to promote it into a Bishop, 'N' for a Knight, 'Q' for a Queen, 'R' for a Rook."); System.out.flush(); this.executor.submit(() -> { PromoteType newPiece; boolean valid = false; do { try { String promotion = scanner.next(); newPiece = switch (promotion) { case "B", "b", "Bishop", "bishop" -> PromoteType.Bishop; case "N", "n", "Knight", "knight" -> PromoteType.Knight; case "Q", "q", "Queen", "queen" -> PromoteType.Queen; case "R", "r", "Rook", "rook" -> PromoteType.Rook; default -> throw new Exception(); }; valid = true; sendPawnPromotion(newPiece); } catch (Exception e) { System.out.println("Invalid input!"); } } while (!valid); }); } /** * Update and print the board in the console. */ @Override public void onBoardUpdate() { if (!this.captureInput) return; StringBuilder string = new StringBuilder(); string.append(" a b c d e f g h \n"); for (int i = 0; i < Coordinate.VALUE_MAX; i++) { string.append(8 - i).append(" "); for (int j = 0; j < Coordinate.VALUE_MAX; j++) { Piece p = getPieceAt(new Coordinate(j, i)); if ((i + j) % 2 == 0) { string.append(Colors.LIGHT_GRAY_BACKGROUND); } else { string.append(Colors.DARK_GRAY_BACKGROUND); } if (p == null) { string.append(" " + Colors.RESET); } else { string.append(" ").append(consolePieceName.getString(p)).append(" ").append(Colors.RESET); } } string.append("\n"); } System.out.println(string); System.out.flush(); } /** * Display the possible moves of a piece. * @param piece * @param moves */ public void displayMoves(Coordinate piece, List moves) { StringBuilder string = new StringBuilder(); string.append(" a b c d e f g h \n"); for (int i = 0; i < Coordinate.VALUE_MAX; i++) { string.append(8 - i).append(" "); for (int j = 0; j < Coordinate.VALUE_MAX; j++) { Coordinate currentCell = new Coordinate(j, i); Piece p = getPieceAt(new Coordinate(j, i)); if (moves.contains(currentCell)) { string.append(Colors.YELLOW_BACKGROUND); } else { if ((i + j) % 2 == 0) { string.append(Colors.LIGHT_GRAY_BACKGROUND); } else { string.append(Colors.DARK_GRAY_BACKGROUND); } } if (p == null) { string.append(" " + Colors.RESET); } else { if (currentCell.equals(piece)) { string.append(Colors.RED_BACKGROUND).append(" ").append(consolePieceName.getString(p)) .append(" ").append(Colors.RESET); } else { string.append(" ").append(consolePieceName.getString(p)).append(" ").append(Colors.RESET); } } } string.append("\n"); } System.out.println(string); } @Override public void onMoveNotAllowed(Move move) { System.out.println("Move not allowed."); } @Override public void onDraw() { System.out.println("Repeated positions!"); } /** * Open different dialogs according to which castling is allowed. * @return true if a castling was played, false otherwise */ private boolean onAskedCastling() { return switch (getAllowedCastlings()) { case Small -> onSmallCastling(); case Big -> onBigCastling(); case Both -> onBothCastling(); default -> { System.out.println("No castling allowed."); yield false; } }; } /** * Ask the user to confirm a small castling. * @return true if the castling was played, false otherwise */ private boolean onSmallCastling() { System.out.println("Small castling allowed. Confirm with \"y\":"); String answer = scanner.nextLine(); if (!(answer.equalsIgnoreCase("y") || answer.equalsIgnoreCase("yes"))) { return false; } else { return (commandExecutor.executeCommand(new CastlingCommand(false)) != Command.CommandResult.Moved); } } /** * Ask the user to confirm a big castling. * @return true if the castling was played, false otherwise */ private boolean onBigCastling() { System.out.println("Big castling allowed. Confirm with \"y\":"); String answer = scanner.nextLine(); if (!(answer.equalsIgnoreCase("y") || answer.equalsIgnoreCase("yes"))) { return false; } else { return (commandExecutor.executeCommand(new CastlingCommand(true)) != Command.CommandResult.Moved); } } /** * Ask the user to pick a castling when both are allowed. * @return true if a castling was played, false otherwise */ private boolean onBothCastling() { System.out.println("Both castlings allowed. Pick \"s\" to play a castling, \"b\" to play a big castling."); String answer = scanner.nextLine(); return switch (answer) { case "s", "S", "small", "Small", "castling", "normal", "Normal" -> (commandExecutor.executeCommand(new CastlingCommand(false)) != Command.CommandResult.Moved); case "b", "B", "big", "Big", "big castling", "Big castling" -> (commandExecutor.executeCommand(new CastlingCommand(true)) != Command.CommandResult.Moved); default -> false; }; } public void setCaptureInput(boolean captureInput) { this.captureInput = captureInput; } @Override public CommandExecutor getCommandExecutor() { return this.commandExecutor; } }