package cs3500.connectn.consoleui; import cs3500.connectn.model.Player; import java.io.IOException; import java.util.*; import static java.util.Objects.requireNonNull; /** * View with console-based UI. */ public final class ConsoleView implements View { /** * Maximum number of columns allowed in the grid to be rendered. */ public static final int MAX_MODEL_WIDTH = 10; /** * Constructs a new {@code ConsoleView.Builder}, which is used to configure * and instantiate new {@code ConsoleView}s. * * @return the new builder */ public static Builder builder() { return new Builder(); } /** * Creates a new console view using the given input and output. * * @param input where to read user input from * @param output where to send output for the user to see */ private ConsoleView(Scanner input, Appendable output) { this.input = input; this.output = output; formatter = new Formatter(output); } private final Scanner input; private final Appendable output; private final Formatter formatter; @Override public void draw(ViewModel vm) throws IOException { if (vm.getWidth() > MAX_MODEL_WIDTH) { throw new IllegalArgumentException("model too wide for view"); } for (int row = vm.getHeight() - 1; row >= 0; --row) { for (int column = 0; column < vm.getWidth(); ++column) { Player token = vm.getPlayerAt(column, row); format("|%c", token == null ? ' ' : token.asChar()); } append("|\n"); } for (int column = 0; column < vm.getWidth(); ++column) { append("+-"); } append("+\n"); for (int column = 0; column < vm.getWidth(); ++column) { format(" %d", column); } append('\n'); } @Override public void announceWinner(Player winner) throws IOException { format(Messages.GAME_OVER); format(Messages.WINNER, winner); } @Override public void announceStalemate() throws IOException { format(Messages.GAME_OVER); format(Messages.STALEMATE); } @Override public int readMove(Player who) throws IOException { // Here we attempt to read one move from the input. We print a prompt, // and then if we can read an int, we return it. Otherwise we consume the // line (which doesn't start with an int) and rethrow the {@code // InputMismatchException} for the controller to deal with (or whoever). try { format(Messages.PROMPT_FMT, who); return input.nextInt(); } catch (InputMismatchException e) { input.nextLine(); throw e; } } @Override public void reportNotANumber() throws IOException { format(Messages.NOT_A_NUMBER); } @Override public void reportColumnIndexOOB(int maxColumn) throws IOException { format(Messages.COLUMN_OOB, maxColumn); } @Override public void reportColumnFull(int column) throws IOException { format(Messages.COLUMN_FULL, column); } private void format(String template, Object... params) throws IOException { formatter.format(template, params); } private void append(CharSequence csq) throws IOException { output.append(csq); } private void append(char c) throws IOException { output.append(c); } /** * Configures and constructs {@link ConsoleView}s. This isn't really * necessary, since the only parameters are the input and output, though it * does help with the overloading on {@code Scanner} versus {@code Readable}. */ public static final class Builder { /** * Configures this {@code Builder} to use the given {@link Scanner} for * input. * * @param input user input source for the view * @return {@code this}, for method chaining */ public Builder input(Scanner input) { this.input = requireNonNull(input); return this; } private Scanner input = new Scanner(System.in); /** * Configures this {@code Builder} to use the given {@link Readable} for * input. * * @param input user input source for the view * @return {@code this}, for method chaining */ public Builder input(Readable input) { return input(new Scanner(requireNonNull(input))); } /** * Configures this {@code Builder} to use the given {@link Appendable} for * output. * * @param output user output sink for the view * @return {@code this}, for method chaining */ public Builder output(Appendable output) { this.output = requireNonNull(output); return this; } private Appendable output = System.out; /** * Constructs and returns a new view as configured by this {@code Builder}. * * @return the new view */ public View build() { return new ConsoleView(input, output); } } /** * Messages used by this view. */ static final class Messages { static final String PROMPT_FMT = "%s: "; static final String NOT_A_NUMBER = "Please enter a number.\n"; static final String COLUMN_OOB = "That column doesn't exist. Choose a column between 0 and %d.\n"; static final String COLUMN_FULL = "Column %d is full. Please choose another.\n"; static final String WINNER = "%s player wins :)\n"; static final String STALEMATE = "Nobody wins :/\n"; static final String GAME_OVER = "Game over!\n"; /** * UTILITY CLASS * Do Not Instantiate */ private Messages() { assert false : "Well I don't know what you expected."; } } }