I need some help debugging my code. I'm going to give you the code and then the bug and you need to fix it.

Code:


package cs3500.solored.controller;

import java.util.ArrayList;
import java.util.List;

import cs3500.solored.model.hw02.Card;
import cs3500.solored.model.hw02.Color;
import cs3500.solored.model.hw02.RedGameModel;
import cs3500.solored.model.hw02.RedSevenCard;

/**
 * Represents a mock for RedGameModel.
 */
public class MockRedGameModel implements RedGameModel<Card> {
  private final List<Card> hand;
  private final List<List<Card>> palettes;
  private final Card canvasCard;
  private final int numPalettes;
  private boolean gameOver;
  private boolean gameWon;

  /**
   * MockRedGameModel constructor.
   * @param numPalettes the number of palettes in the game
   * @param handSize the size of the hand
   */
  public MockRedGameModel(int numPalettes, int handSize) {
    this.hand = new ArrayList<>();
    this.palettes = new ArrayList<>();
    for (int i = 0; i < numPalettes; i++) {
      this.palettes.add(new ArrayList<>());
    }
    this.canvasCard = new RedSevenCard(Color.RED, 7);
    this.numPalettes = numPalettes;
    this.gameOver = false;
    this.gameWon = false;
  }

  @Override
  public void startGame(List<Card> deck, boolean shuffle, int numPalettes, int handSize) {
    // Mock implementation
  }

  @Override
  public void playToPalette(int paletteIdx, int cardIdxInHand) {
    // Mock implementation
  }

  @Override
  public void playToCanvas(int cardIdxInHand) {
    // Mock implementation
  }

  @Override
  public void drawForHand() {
    // Mock implementation
  }

  @Override
  public int numOfCardsInDeck() {
    return 0; // Mock implementation
  }

  @Override
  public int numPalettes() {
    return numPalettes;
  }

  @Override
  public int winningPaletteIndex() {
    return 0; // Mock implementation
  }

  @Override
  public boolean isGameOver() {
    return gameOver;
  }

  @Override
  public boolean isGameWon() {
    return gameWon;
  }

  @Override
  public List<Card> getHand() {
    return new ArrayList<>(hand);
  }

  @Override
  public List<Card> getPalette(int paletteNum) {
    return palettes.get(paletteNum);
  }

  @Override
  public Card getCanvas() {
    return canvasCard;
  }

  @Override
  public List<Card> getAllCards() {
    return new ArrayList<>(); // Mock implementation
  }

}

package cs3500.solored.controller;

import cs3500.solored.model.hw02.Card;
import cs3500.solored.model.hw02.RedGameModel;

import java.util.List;

/**
 * Controller for RedGame.
 */
public interface RedGameController {
  /**
   * Plays a new game of Red7 using the provided model.
   *
   * @param model        the game model to use
   * @param deck         the deck of cards to use
   * @param shuffle      whether to shuffle the deck
   * @param numPalettes  the number of palettes
   * @param handSize     the size of the hand
   * @param <C>          the type of cards used in the game
   * @throws IllegalArgumentException if the provided model is invalid or the game cannot be started
   * @throws IllegalStateException    if the controller is unable to receive/transmit input/output
   */
  <C extends Card> void playGame(RedGameModel<C> model, List<C> deck, boolean shuffle,
                                 int numPalettes, int handSize)
          throws IllegalArgumentException, IllegalStateException;
}

package cs3500.solored.controller;

import java.io.IOException;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Scanner;

import cs3500.solored.model.hw02.Card;
import cs3500.solored.model.hw02.RedGameModel;
import cs3500.solored.view.hw02.RedGameView;
import cs3500.solored.view.hw02.SoloRedGameTextView;

/**
 * Controls the flow of the game of SoloRed.
 */
public class SoloRedTextController implements RedGameController {
  private final Readable readable;
  private final Appendable appendable;

  /**
   * Constructor for SoloRedTextController.
   *
   * @param readable  the Readable object to read input from
   * @param appendable the Appendable object to append output to
   * @throws IllegalArgumentException if either readable or appendable is null
   */
  public SoloRedTextController(Readable readable, Appendable appendable)
          throws IllegalArgumentException {
    if (readable == null || appendable == null) {
      throw new IllegalArgumentException("Readable and Appendable cannot be null");
    }
    this.readable = readable;
    this.appendable = appendable;
  }

  @Override
  public <C extends Card> void playGame(RedGameModel<C> model, List<C> deck,
                                        boolean shuffle, int numPalettes, int handSize) {
    if (model == null) {
      throw new IllegalArgumentException("Model cannot be null");
    }

    try {
      model.startGame(deck, shuffle, numPalettes, handSize);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("Game cannot be started: " + e.getMessage());
    }

    Scanner scnr = new Scanner(this.readable);
    RedGameView view = new SoloRedGameTextView(model, this.appendable);

    try {
      while (!model.isGameOver()) {
        view.render();
        this.appendable.append(String.format("Number of cards in deck: "
                + model.numOfCardsInDeck() +  "\n"));

        String command = scnr.next();

        if (command.equalsIgnoreCase("q")) {
          handleQuit(model, view);
          return;
        } else if (command.equalsIgnoreCase("palette")) {
          int paletteIndex = readInt(scnr) - 1;
          if (paletteIndex == -2) {
            handleQuit(model, view);
            return;
          }
          int cardIndex = readInt(scnr) - 1;
          if (cardIndex == -2) {
            handleQuit(model, view);
            return;
          }
          try {
            model.playToPalette(paletteIndex, cardIndex);
          } catch (IllegalArgumentException | IllegalStateException e) {
            this.appendable.append("Invalid move. Try again. ").append(e.getMessage()).append("\n");
          }
        } else if (command.equalsIgnoreCase("canvas")) {
          int cardIndex = readInt(scnr) - 1;
          if (cardIndex == -2) {
            handleQuit(model, view);
            return;
          }
          try {
            model.playToCanvas(cardIndex);
          } catch (IllegalArgumentException | IllegalStateException e) {
            this.appendable.append("Invalid move. Try again. ").append(e.getMessage()).append("\n");
          }
        } else {
          this.appendable.append("Invalid command. Try again. Valid commands are: palette, canvas, q\n");
        }
      }

      if (model.isGameWon()) {
        this.appendable.append("Game won.\n");
      } else {
        this.appendable.append("Game lost.\n");
      }
      view.render();
      this.appendable.append(String.format("Number of cards in deck: %d\n", model.numOfCardsInDeck()));

    } catch (IOException ioe) {
      throw new IllegalStateException("Error during input/output", ioe);
    } catch (NoSuchElementException e) {
      throw new IllegalStateException("No more input available", e);
    }
  }

  // Helper method to read an integer with retries and quit handling
  private int readInt(Scanner scanner) throws IOException {
    while (true) { // Keep looping until a valid integer or 'q'/'Q' is entered
      if (!scanner.hasNext()) {
        throw new IllegalStateException("No more input available");
      }

      String input = scanner.next();

      if (input.equalsIgnoreCase("q")) { // User wants to quit
        return -2; // Return a special value to signal quit
      }

      try {
        return Integer.parseInt(input); // Return the valid integer
      } catch (NumberFormatException e) {
        this.appendable.append("Invalid input. Please enter an integer or 'q' to quit.\n");
      }
    }
  }

  // Helper method to handle quit command
  private <C extends Card> void handleQuit(RedGameModel<C> model, RedGameView view)
          throws IOException {
    this.appendable.append("Game quit!\nState of game when quit:\n");
    view.render();
    this.appendable.append(String.format("Number of cards in deck: %d\n",
            model.numOfCardsInDeck()));
  }
}

package cs3500.solored.controller;

import cs3500.solored.model.hw02.Canvas;
import cs3500.solored.model.hw02.Card;
import cs3500.solored.model.hw02.Color;
import cs3500.solored.model.hw02.Palette;
import cs3500.solored.model.hw02.RedGameModel;
import cs3500.solored.model.hw02.RedSevenCard;
import cs3500.solored.model.hw02.WinningRule;

import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Tests for the SoloRedTextController class.
 */
public class SoloRedTextControllerTests {

  private RedGameModel<Card> model;
  private List<Card> deck;
  private StringWriter out;

  @Before
  public void setUp() {
    model = new MockRedGameModel(4, 7);
    deck = model.getAllCards();
    out = new StringWriter();
  }

  @Test
  public void testConstructorWithNullReadable() {
    try {
      new SoloRedTextController(null, new StringBuilder());
      fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException e) {
      // Expected
    }
  }

  @Test
  public void testConstructorWithNullAppendable() {
    try {
      new SoloRedTextController(new StringReader(""), null);
      fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException e) {
      // Expected
    }
  }

  @Test
  public void testPlayGameWithNullModel() {
    String input = "q\n";
    StringReader in = new StringReader(input);
    StringBuilder out = new StringBuilder();

    RedGameController controller = new SoloRedTextController(in, out);
    try {
      controller.playGame(null, deck, false, 4, 7);
      fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException e) {
      // Expected
    }
  }

  @Test
  public void testPlayGameWithIOException() {
    String input = "q\n";
    StringReader in = new StringReader(input);
    Appendable out = new Appendable() {
      @Override
      public Appendable append(CharSequence csq) throws IOException {
        throw new IOException("Test exception");
      }

      @Override
      public Appendable append(CharSequence csq, int start, int end) throws IOException {
        throw new IOException("Test exception");
      }

      @Override
      public Appendable append(char c) throws IOException {
        throw new IOException("Test exception");
      }
    };

    RedGameController controller = new SoloRedTextController(in, out);
    try {
      controller.playGame(model, deck, false, 4, 7);
      fail("Expected IllegalStateException");
    } catch (IllegalStateException e) {
      // Expected
    }
  }

  @Test
  public void testRenderOutputEndsWithNewline() throws IOException {
    String input = "q\n";
    StringReader in = new StringReader(input);
    RedGameController controller = new SoloRedTextController(in, out);
    controller.playGame(model, deck, false, 4, 7);

    String output = out.toString();
    assertTrue("Render output does not end with a newline", output.endsWith("\n"));
  }

  @Test
  public void testDetermineWinningPalette_OrangeRuleTieBreaker() {
    // Set up palettes with a tie for the Orange rule (most of a single number)
    List<Palette> palettes = new ArrayList<>();

    // Palette 1: Two 3s (and a higher card)
    Palette palette1 = new Palette();
    palette1.addCard(new RedSevenCard(Color.ORANGE, 3));
    palette1.addCard(new RedSevenCard(Color.BLUE, 3));
    palette1.addCard(new RedSevenCard(Color.RED, 7)); // Highest card

    // Palette 2: Two 3s (but a lower card)
    Palette palette2 = new Palette();
    palette2.addCard(new RedSevenCard(Color.ORANGE, 3));
    palette2.addCard(new RedSevenCard(Color.VIOLET, 3));
    palette2.addCard(new RedSevenCard(Color.INDIGO, 5));

    palettes.add(palette1);
    palettes.add(palette2);

    // Set up Canvas with Orange card, (hopefully lol) triggers orange rule
    Canvas canvas = new Canvas(new RedSevenCard(Color.ORANGE, 1));

    int winningPaletteIndex = WinningRule.determineWinningPalette(palettes, canvas);
    assertEquals(0, winningPaletteIndex);
  }

}

package cs3500.solored.model.hw02;

/**
 * Represents the canvas in the RedSeven game.
 */
public class Canvas {
  private Card card;

  /**
   * Constructs a new Canvas with the given card.
   *
   * @param card the card to init the canvas with.
   */
  public Canvas(Card card) {
    this.card = card;
  }

  /**
   * Returns the card on the canvas.
   *
   * @return the card on the canvas.
   */
  public Card getCard() {
    return card;
  }

  /**
   * Sets the card on the canvas.
   *
   * @param card the card to place on the canvas.
   */
  public void placeCard(Card card) {
    this.card = card;
  }
}

package cs3500.solored.model.hw02;

/**
 * Behaviors for a Card in the Game of RedSeven.
 * Any additional behaviors for cards must be made
 * creating a new interface that extends this one.
 */
public interface Card {

  /**
   * Prints the color and number of the card.
   * The colors are printed R, O, B, I, or V.
   * The numbers are printed as 1-7.
   * As an example, a blue 5 is printed as B5.
   * @return a two character representation of the card
   */
  String toString();
}

package cs3500.solored.model.hw02;

/**
 * Represents the possible colors of a card in the game.
 */
public enum Color {
  RED("R"),
  ORANGE("O"),
  BLUE("B"),
  INDIGO("I"),
  VIOLET("V");

  private final String abbrev; // stands for abbreviation

  Color(String abbrev) {
    if (abbrev == null) {
      throw new IllegalStateException("Color cannot be null");
    }
    this.abbrev = abbrev;
  }

  @Override
  public String toString() {
    return abbrev;
  }

}

package cs3500.solored.model.hw02;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

/**
 * Represents the deck of cards in the RedSeven game.
 */
public class Deck {
  private final List<RedSevenCard> cards;

  /**
   * Constructs a new Deck with the given list of cards.
   *
   * @param cards the list of cards to initialize the deck with.
   */
  public Deck(List<RedSevenCard> cards) {
    this.cards = new ArrayList<>(cards);
  }

  /**
   * Shuffles the deck using a provided Random object.
   *
   * @param random the Random object to use for shuffling.
   */
  public void shuffle(Random random) {
    Collections.shuffle(cards, random);
  }

  /**
   * Draws a card from the top of the deck.
   *
   * @return the top card from the deck.
   * @throws IllegalStateException if the deck is empty.
   */
  public RedSevenCard draw() {
    if (isEmpty()) {
      throw new IllegalStateException("Deck is empty");
    }
    return cards.remove(0);
  }

  /**
   * Checks if the deck is empty.
   *
   * @return if the deck is empty.
   */
  public boolean isEmpty() {
    return cards.isEmpty();
  }

  /**
   * Returns the number of cards remaining in the deck.
   *
   * @return the number of remaining cards in the deck.
   */
  public int size() {
    return cards.size();
  }

  /**
   * Returns a copy of the current deck.
   *
   * @return a new list containing the cards in the deck.
   */
  public List<RedSevenCard> getCards() {
    return new ArrayList<>(cards);
  }
}

package cs3500.solored.model.hw02;

import java.util.ArrayList;
import java.util.List;

/**
 * Represents a palette in the RedSeven game.
 */
public class Palette {

  // The cards in the palette. The card at index 0 is the first card played,
  // and the card at the highest index, cards.length-1, is the last card played.
  private final List<RedSevenCard> cards;

  /**
   * Constructs a new Palette.
   */
  public Palette() {
    this.cards = new ArrayList<>();
  }

  /**
   * Adds a card to the palette.
   *
   * @param card the card to add to the palette.
   */
  public void addCard(RedSevenCard card) {
    cards.add(card);
  }

  /**
   * Returns a copy of the cards in the palette.
   *
   * @return a new list containing the cards in the palette.
   */
  public List<RedSevenCard> getCards() {
    return new ArrayList<>(cards);
  }

  /**
   * Returns the number of cards in the palette.
   *
   * @return the number of cards in the palette.
   */
  public int size() {
    return cards.size();
  }
}

package cs3500.solored.model.hw02;

import java.util.List;

/**
 * Behaviors for a solo game of RedSeven.
 * The game consists of four structures:
 * <ul>
 *   <li>A deck of cards to draw from</li>
 *   <li>A hand to play from</li>
 *   <li>Four palettes to play to</li>
 *   <li>A canvas that dictates the winning rule</li>
 * </ul>
 * The goal of the game is to use all the cards in the deck
 * while ensuring exactly one palette is winning each round.
 *
 * @param <C> the type of cards used
 */
public interface RedGameModel<C extends Card> {

  /**
   * Play the given card from the hand to the losing palette chosen.
   * The card is removed from the hand and placed at the far right
   * end of the palette.
   *
   * @param paletteIdx    a 0-index number representing which palette to play to
   * @param cardIdxInHand a 0-index number representing the card to play from the hand
   * @throws IllegalStateException    if the game has not started or the game is over
   * @throws IllegalArgumentException if paletteIdx < 0 or more than the number of palettes
   * @throws IllegalArgumentException if cardIdxInHand < 0
   *                                  or greater/equal to the number of cards in hand
   * @throws IllegalStateException    if the palette referred to by paletteIdx is winning
   */
  void playToPalette(int paletteIdx, int cardIdxInHand);

  /**
   * Play the given card from the hand to the canvas.
   * This changes the rules of the game for all palettes.
   * The method can only be called once per turn.
   *
   * @param cardIdxInHand a 0-index number representing the card to play from the hand
   * @throws IllegalStateException    if the game has not started or the game is over
   * @throws IllegalArgumentException if cardIdxInHand < 0
   *                                  or greater/equal to the number of cards in hand
   * @throws IllegalStateException    if this method was already called once in a given turn
   * @throws IllegalStateException    if there is exactly one card in hand
   */
  void playToCanvas(int cardIdxInHand);

  /**
   * Draws cards from the deck until the hand is full
   * OR until the deck is empty, whichever occurs first. Newly drawn cards
   * are added to the end of the hand (far-right conventionally).
   * SIDE-EFFECT: Allows the player to play to the canvas again.
   *
   * @throws IllegalStateException if the game has not started or the game is over
   */
  void drawForHand();

  /**
   * Starts the game with the given options. The deck given is used
   * to set up the palettes and hand. Modifying the deck given to this method
   * will not modify the game state in any way.
   *
   * @param deck        the cards used to set up and play the game
   * @param shuffle     whether the deck should be shuffled prior to setting up the game
   * @param numPalettes number of palettes in the game
   * @param handSize    the maximum number of cards allowed in the hand
   * @throws IllegalStateException    if the game has started or the game is over
   * @throws IllegalArgumentException if numPalettes < 2 or handSize <= 0
   * @throws IllegalArgumentException if deck's size is not large enough to setup the game
   * @throws IllegalArgumentException if deck has non-unique cards or null cards
   */
  void startGame(List<C> deck, boolean shuffle, int numPalettes, int handSize);

  /**
   * Returns the number of cards remaining in the deck used in the game.
   *
   * @return the number of cards in the deck
   * @throws IllegalStateException if the game has not started
   */
  int numOfCardsInDeck();

  /**
   * Returns the number of palettes in the running game.
   *
   * @return the number of palettes in the game
   * @throws IllegalStateException if the game has not started
   */
  int numPalettes();

  /**
   * Returns the index of the winning palette in the game.
   *
   * @return the 0-based index of the winning palette
   * @throws IllegalStateException if the game has not started
   */
  int winningPaletteIndex();

  /**
   * Returns if the game is over as specified by the implementation.
   *
   * @return true if the game has ended and false otherwise
   * @throws IllegalStateException if the game has not started
   */
  boolean isGameOver();

  /**
   * Returns if the game is won by the player as specified by the implementation.
   *
   * @return true if the game has been won or false if the game has not
   * @throws IllegalStateException if the game has not started or the game is not over
   */
  boolean isGameWon();

  /**
   * Returns a copy of the hand in the game. This means modifying the returned list
   * or the cards in the list has no effect on the game.
   *
   * @return a new list containing the cards in the player's hand in the same order
   * as in the current state of the game.
   * @throws IllegalStateException if the game has not started
   */
  List<C> getHand();

  /**
   * Returns a copy of the specified palette. This means modifying the returned list
   * or the cards in the list has no effect on the game.
   *
   * @param paletteNum 0-based index of a particular palette
   * @return a new list containing the cards in specified palette in the same order
   * as in the current state of the game.
   * @throws IllegalStateException    if the game has not started
   * @throws IllegalArgumentException if paletteIdx < 0 or more than the number of palettes
   */
  List<C> getPalette(int paletteNum);

  /**
   * Return the top card of the canvas.
   * Modifying this card has no effect on the game.
   *
   * @return the top card of the canvas
   * @throws IllegalStateException if the game has not started
   */
  C getCanvas();

  /**
   * Get a NEW list of all cards that can be used to play the game.
   * Editing this list should have no effect on the game itself.
   * Repeated calls to this method should produce a list of cards in the same order.
   * Modifying the cards in this list should have no effect on any returned list
   * or the game itself.
   *
   * @return a new list of all possible cards that can be used for the game
   */
  List<C> getAllCards();
}

package cs3500.solored.model.hw02;

/**
 * Represents a card in the RedSeven game.
 */
public class RedSevenCard implements Card {

  private final Color color;
  private final int number;

  /**
   * RedSevenCard constructor.
   *
   * @param color color of the card.
   * @param number number of the card.
   * @throws IllegalArgumentException if the number is invalid.
   */
  public RedSevenCard(Color color, int number) {
    if (number < 1 || number > 7) {
      throw new IllegalArgumentException("Invalid number: " + number);
    }
    this.color = color;
    this.number = number;
  }

  @Override
  public String toString() {
    return color.toString() + number;
  }

  public Color getColor() {
    return color;
  }

  public int getNumber() {
    return number;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    RedSevenCard that = (RedSevenCard) o;
    return number == that.number && color == that.color;
  }

  @Override
  public int hashCode() {
    int result = color.hashCode();
    result = 31 * result + number;
    return result;
  }
}

package cs3500.solored.model.hw02;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * Implementation of the RedGameModel interface for a game of Red7.
 */
public class SoloRedGameModel implements RedGameModel<RedSevenCard> {

  private Deck deck;
  private List<RedSevenCard> hand;
  private List<Palette> palettes;
  private Canvas canvas;
  private boolean gameStarted;
  private boolean gameOver;
  private boolean gameWon;
  private int numPalettes;
  private int handSize;
  private boolean canvasPlayedThisTurn;
  private Random random;

  /**
   * Default constructor. Inits the game conditions.
   */
  public SoloRedGameModel() {
    this.random = new Random();
  }

  /**
   * Constructor with a Random object for shuffling.
   *
   * @param random the Random object to use for shuffling.
   * @throws IllegalArgumentException if the Random object is null.
   */
  public SoloRedGameModel(Random random) {
    if (random == null) {
      throw new IllegalArgumentException("Random object cannot be null");
    }
    this.random = random;
  }

  /**
   * Starts the game with the given options.
   *
   * @param deck        the cards used to set up and play the game.
   * @param shuffle     whether the deck should be shuffled prior to setting up the game.
   * @param numPalettes number of palettes in the game.
   * @param handSize    the maximum number of cards allowed in the hand..
   * @throws IllegalStateException    if the game has started or the game is over.
   * @throws IllegalArgumentException if numPalettes < 2 or handSize <= 0.
   * @throws IllegalArgumentException if deck's size is not large enough to setup the game.
   * @throws IllegalArgumentException if deck has non-unique cards or null cards.
   */
  @Override
  public void startGame(List<RedSevenCard> deck, boolean shuffle, int numPalettes, int handSize) {
    if (deck == null) {
      throw new IllegalArgumentException("Deck cannot be null");
    }
    if (gameStarted || gameOver) {
      throw new IllegalStateException("Game has already started or is over");
    }
    if (numPalettes < 2 || handSize <= 0) {
      throw new IllegalArgumentException("Invalid number of palettes or hand size");
    }
    if (deck.size() < numPalettes + handSize) {
      throw new IllegalArgumentException("Deck size is not large enough to setup the game");
    }
    if (!isUniqueAndNotNull(deck)) {
      throw new IllegalArgumentException("Deck contains non-unique or null cards");
    }

    this.deck = new Deck(deck);
    if (shuffle) {
      this.deck.shuffle(random);
    }
    this.hand = new ArrayList<>();
    this.palettes = new ArrayList<>();
    this.numPalettes = numPalettes;
    this.handSize = handSize;
    this.gameStarted = true;
    this.gameOver = false;
    this.gameWon = false;
    this.canvasPlayedThisTurn = false;

    setupGame();
  }

  /**
   * Sets up the game. Creates palettes and adds one card to each palette.
   * Init the canvas with a Red card and drawing cards to fill the hand.
   */
  private void setupGame() {
    for (int i = 0; i < numPalettes; i++) {
      Palette palette = new Palette();
      palette.addCard(deck.draw());
      palettes.add(palette);
    }

    canvas = new Canvas(new RedSevenCard(Color.RED, 7)); // Assume Red 7 as a placeholder

    drawForHand();
  }

  /**
   * Plays the given card to the specified palette.
   * The card is removed from the hand and placed at right end of the palette.
   *
   * @param paletteIndex    a 0-index number representing which palette to play to.
   * @param cardIndexInHand a 0-index number representing the card to play from the hand..
   * @throws IllegalStateException    if the game has not started or the game is over.
   * @throws IllegalArgumentException if paletteIndex < 0 or more than number of palettes.
   * @throws IllegalArgumentException if cardIndexInHand < 0.
   *                                  or >= to the number of cards in hand.
   * @throws IllegalStateException    if the palette[paletteIndex] is winning.
   */
  @Override
  public void playToPalette(int paletteIndex, int cardIndexInHand) {
    if (!gameStarted || gameOver) {
      throw new IllegalStateException("Game has not started or is over");
    }
    if (paletteIndex < 0 || paletteIndex >= numPalettes) {
      throw new IllegalArgumentException("Invalid palette index");
    }
    if (cardIndexInHand < 0 || cardIndexInHand >= hand.size()) {
      throw new IllegalArgumentException("Invalid card index in hand");
    }
    if (paletteIndex == winningPaletteIndex()) {
      throw new IllegalStateException("Cannot play to the winning palette");
    }

    RedSevenCard card = hand.remove(cardIndexInHand);
    palettes.get(paletteIndex).addCard(card);

    // Check if the palette is now winning
    if (paletteIndex != winningPaletteIndex()) {
      gameOver = true;
      gameWon = false;
    } else {
      // Only draw if the palette remains the winning palette after playing le card
      drawForHand();
    }

    // Check for game over (win condition) after (potentially) drawing a card
    if (hand.isEmpty() && deck.isEmpty()) {
      gameOver = true;
      gameWon = true;
    }
  }

  /**
   * Plays the given card from the hand to the canvas.
   * Changes the rules of the game.
   *
   * @param cardIndexInHand a 0-index number representing the card to play from the hand.
   * @throws IllegalStateException    if the game has not started or the game is over.
   * @throws IllegalArgumentException if cardIndexInHand < 0.
   *                                  or greater/equal to the number of cards in hand.
   * @throws IllegalStateException    if this method was already called once in a given turn.
   * @throws IllegalStateException    if there is exactly one card in hand.
   */
  @Override
  public void playToCanvas(int cardIndexInHand) {
    if (!gameStarted || gameOver) {
      throw new IllegalStateException("Game has not started or is over");
    }
    if (cardIndexInHand < 0 || cardIndexInHand >= hand.size()) {
      throw new IllegalArgumentException("Invalid card index in hand");
    }
    if (canvasPlayedThisTurn) {
      throw new IllegalStateException("Canvas has already been played this turn");
    }
    if (hand.size() == 1) {
      throw new IllegalStateException("Cannot play to canvas with only one card in hand");
    }

    RedSevenCard card = hand.remove(cardIndexInHand);
    canvas.placeCard(card);
    canvasPlayedThisTurn = true;

    // Always draw after playing to the canvas
    drawForHand();
  }

  public void endTurn() {
    drawForHand();
    canvasPlayedThisTurn = false;
  }

  /**
   * Draws cards from the deck until the hand is full or until the deck is empty.
   *
   * @throws IllegalStateException if the game has not started or the game is over.
   */
  @Override
  public void drawForHand() {
    if (!gameStarted || gameOver) {
      throw new IllegalStateException("Game has not started or is over");
    }
    while (hand.size() < handSize && !deck.isEmpty()) {
      hand.add(deck.draw());
    }
    canvasPlayedThisTurn = false;
  }

  /**
   * Returns the number of cards remaining in the deck.
   *
   * @return the number of cards in the deck.
   * @throws IllegalStateException if the game has not started.
   */
  @Override
  public int numOfCardsInDeck() {
    if (!gameStarted) {
      throw new IllegalStateException("Game has not started");
    }
    return deck.size();
  }

  /**
   * Returns the number of palettes in the game.
   *
   * @return the number of palettes in the game.
   * @throws IllegalStateException if the game has not started.
   */
  @Override
  public int numPalettes() {
    if (!gameStarted) {
      throw new IllegalStateException("Game has not started");
    }
    return numPalettes;
  }

  /**
   * Returns the index of the winning palette.
   *
   * @return the 0-based index of the winning palette.
   * @throws IllegalStateException if the game has not started.
   */
  @Override
  public int winningPaletteIndex() {
    if (!gameStarted) {
      throw new IllegalStateException("Game has not started");
    }
    return WinningRule.determineWinningPalette(palettes, canvas);
  }

  /**
   * Returns if the game is over.
   *
   * @return if the game is over.
   * @throws IllegalStateException if the game has not started.
   *                               Game can't be over if it hasn't started.
   */
  @Override
  public boolean isGameOver() {
    if (!gameStarted) {
      throw new IllegalStateException("Game has not started");
    }
    return gameOver;
  }

  /**
   * Returns if the game is won by the player.
   *
   * @return if the game is won.
   * @throws IllegalStateException if the game has not started or the game is not over.
   */
  @Override
  public boolean isGameWon() {
    if (!gameStarted || !gameOver) {
      throw new IllegalStateException("Game has not started or is not over");
    }
    return gameWon;
  }

  /**
   * Returns a copy of the hand in the game.
   *
   * @return a new list containing the cards in the player's hand in the same order
   *         as in the current state of the game.
   * @throws IllegalStateException if the game has not started
   */
  @Override
  public List<RedSevenCard> getHand() {
    if (!gameStarted) {
      throw new IllegalStateException("Game has not started");
    }
    return new ArrayList<>(hand);
  }

  /**
   * Returns a copy of the specified palette.
   *
   * @param paletteNum 0-based index of a particular palette.
   * @return new list of cards in the specific palette.
   * @throws IllegalStateException    if the game has not started.
   * @throws IllegalArgumentException if paletteNum < 0 or more than the number of palettes.
   */
  @Override
  public List<RedSevenCard> getPalette(int paletteNum) {
    if (!gameStarted) {
      throw new IllegalStateException("Game has not started");
    }
    if (paletteNum < 0 || paletteNum >= numPalettes) {
      throw new IllegalArgumentException("Invalid palette index");
    }
    return palettes.get(paletteNum).getCards();
  }

  /**
   * Returns the top card of the canvas.
   *
   * @return the top card of the canvas
   * @throws IllegalStateException if the game has not started.
   */
  @Override
  public RedSevenCard getCanvas() {
    if (!gameStarted) {
      throw new IllegalStateException("Game has not started");
    }
    return (RedSevenCard) canvas.getCard();
  }

  /**
   * Get a new list of all cards in this game.
   *
   * @return a new list of all possible cards that can be used for the game
   */
  @Override
  public List<RedSevenCard> getAllCards() {
    List<RedSevenCard> allCards = new ArrayList<>();
    for (Color color : Color.values()) {
      for (int number = 1; number <= 7; number++) {
        allCards.add(new RedSevenCard(color, number));
      }
    }
    return allCards;
  }

  /**
   * Checks if the given deck contains unique and non-null cards.
   *
   * @param deck the list of cards to check.
   * @return true if all cards in the deck are unique and non-null.
   */
  private boolean isUniqueAndNotNull(List<RedSevenCard> deck) {
    List<RedSevenCard> uniqueCards = new ArrayList<>();
    for (RedSevenCard card : deck) {
      if (card == null || uniqueCards.contains(card)) {
        return false;
      }
      uniqueCards.add(card);
    }
    return true;
  }

  /**
   * Draws one card to the hand.
   */
  public void drawOneCard() {
    if (!deck.isEmpty()) {
      hand.add(deck.draw());
    }
  }

  /**
   * Returns the deck.
   *
   * @return the current deck.
   */
  public Deck getDeck() {
    return deck;
  }
}

package cs3500.solored.model.hw02;

import java.util.ArrayList;
import java.util.List;

/**
 * "Helper" class to determine the winning palette based on the current rule.
 */
public class WinningRule {

  /**
   * Determines the winning palette based on the current rule.
   *
   * @param palettes list of palettes to evaluate.
   * @param canvas   the canvas card that dictates the rule.
   * @return index of the winning palette.
   */
  public static int determineWinningPalette(List<Palette> palettes, Canvas canvas) {

    String canvasCardString = canvas.getCard().toString();
    Color canvasColor = parseColor(canvasCardString);

    switch (canvasColor) {
      case RED:
        return highestCardRule(palettes);
      case ORANGE:
        return mostOfSingleNumberRule(palettes);
      case BLUE:
        return mostDifferentColorsRule(palettes);
      case INDIGO:
        return longestRunRule(palettes);
      case VIOLET:
        return mostCardsBelowFourRule(palettes);
      default:
        throw new IllegalArgumentException("Unknown color on canvas: " + canvasColor);
    }
  }

  /**
   * Determines the Color of the card.
   * Purpose: can't modify `Card` interface
   * @param cardString card representation as a string
   * @return the `Color` (the class, not string) of the card
   */
  private static Color parseColor(String cardString) {
    String colorChar = cardString.substring(0, 1);
    switch (colorChar) {
      case "R":
        return Color.RED;
      case "O":
        return Color.ORANGE;
      case "B":
        return Color.BLUE;
      case "I":
        return Color.INDIGO;
      case "V":
        return Color.VIOLET;
      default:
        throw new IllegalArgumentException("Unknown color: " + colorChar);
    }
  }

  private static int parseNumber(String cardString) {
    return Integer.parseInt(cardString.substring(1));
  }

  /**
   * Determines the winning palette based on the highest card rule.
   *
   * @param palettes the list of palettes to evaluate.
   * @return index of the winning palette.
   */
  private static int highestCardRule(List<Palette> palettes) {
    int winningIndex = 0;
    RedSevenCard highestCard = null;
    for (int i = 0; i < palettes.size(); i++) {
      Palette palette = palettes.get(i);
      for (RedSevenCard card : palette.getCards()) {
        if (highestCard == null || isHigher(card, highestCard)) {
          highestCard = card;
          winningIndex = i;
        }
      }
    }
    return winningIndex;
  }

  /**
   * Determines the winning palette based on the most of a single number rule.
   *
   * @param palettes list of palettes to evaluate.
   * @return index of the winning palette.
   */
  private static int mostOfSingleNumberRule(List<Palette> palettes) {
    int winningIndex = 0;
    int maxCount = 0;
    List<Integer> tiedIndices = new ArrayList<>();
    for (int i = 0; i < palettes.size(); i++) {
      Palette palette = palettes.get(i);
      int[] counts = new int[8]; // size 8 bc 8 possible numbers
      for (RedSevenCard card : palette.getCards()) {
        counts[card.getNumber()]++;
      }
      for (int count : counts) {
        if (count > maxCount) {
          maxCount = count;
          winningIndex = i;
          tiedIndices.clear();
          tiedIndices.add(i);
        } else if (count == maxCount) {
          tiedIndices.add(i);
        }
      }
    }
    if (tiedIndices.size() > 1) {
      return breakTie(palettes, tiedIndices);
    }
    return winningIndex;
  }

  /**
   * Determines the winning palette based on the most different colors rule.
   *
   * @param palettes the list of palettes to evaluate.
   * @return index of the winning palette.
   */
  private static int mostDifferentColorsRule(List<Palette> palettes) {
    int winningIndex = 0;
    int maxDifferentColors = 0;
    List<Integer> tiedIndices = new ArrayList<>();
    for (int i = 0; i < palettes.size(); i++) {
      Palette palette = palettes.get(i);
      boolean[] colors = new boolean[5]; // size 5 bc 5 colors: R, O, B, I, V
      for (RedSevenCard card : palette.getCards()) {
        colors[card.getColor().ordinal()] = true;
      }
      int differentColors = 0;
      for (boolean color : colors) {
        if (color) {
          differentColors++;
        }
      }
      if (differentColors > maxDifferentColors) {
        maxDifferentColors = differentColors;
        winningIndex = i;
        tiedIndices.clear();
        tiedIndices.add(i);
      } else if (differentColors == maxDifferentColors) {
        tiedIndices.add(i);
      }
    }
    if (tiedIndices.size() > 1) {
      return breakTie(palettes, tiedIndices);
    }
    return winningIndex;
  }

  /**
   * Determines the winning palette based on the longest run rule.
   *
   * @param palettes list of palettes to evaluate.
   * @return index of the winning palette.
   */
  private static int longestRunRule(List<Palette> palettes) {
    int winningIndex = 0;
    int maxRunLength = 0;
    List<Integer> tiedIndices = new ArrayList<>();
    for (int i = 0; i < palettes.size(); i++) {
      Palette palette = palettes.get(i);
      List<RedSevenCard> cards = palette.getCards();
      int runLength = calculateRunLength(cards);
      if (runLength > maxRunLength) {
        maxRunLength = runLength;
        winningIndex = i;
        tiedIndices.clear();
        tiedIndices.add(i);
      } else if (runLength == maxRunLength) {
        tiedIndices.add(i);
      }
    }
    if (tiedIndices.size() > 1) {
      return breakTie(palettes, tiedIndices);
    }
    return winningIndex;
  }

  /**
   * Determines the winning palette based on value of 4 rule.
   *
   * @param palettes the list of palettes to evaluate.
   * @return the index of the winning palette.
   */
  private static int mostCardsBelowFourRule(List<Palette> palettes) {
    int winningIndex = 0;
    int maxBelowFour = 0;
    List<Integer> tiedIndices = new ArrayList<>();
    for (int i = 0; i < palettes.size(); i++) {
      Palette palette = palettes.get(i);
      int belowFour = 0;
      for (RedSevenCard card : palette.getCards()) {
        if (card.getNumber() < 4) {
          belowFour++;
        }
      }
      if (belowFour > maxBelowFour) {
        maxBelowFour = belowFour;
        winningIndex = i;
        tiedIndices.clear();
        tiedIndices.add(i);
      } else if (belowFour == maxBelowFour) {
        tiedIndices.add(i);
      }
    }
    if (tiedIndices.size() > 1) {
      return breakTie(palettes, tiedIndices);
    }
    return winningIndex;
  }

  /**
   * Breaks a tie between palettes by Red Rule.
   *
   * @param palettes    list of palettes to evaluate
   * @param tiedIndices indices of the tied palettes
   * @return index of the winning palette after tiebreak
   */
  private static int breakTie(List<Palette> palettes, List<Integer> tiedIndices) {
    int winningIndex = tiedIndices.get(0);
    RedSevenCard highestCard = null;
    for (int index : tiedIndices) {
      Palette palette = palettes.get(index);
      for (RedSevenCard card : palette.getCards()) {
        if (highestCard == null || isHigher(card, highestCard)) {
          highestCard = card;
          winningIndex = index;
        }
      }
    }
    return winningIndex;
  }

  /**
   * Calculates the length of the longest run in a list of cards.
   *
   * @param cards the list of cards to evaluate
   * @return the length of the longest run
   */
  private static int calculateRunLength(List<RedSevenCard> cards) {
    int[] counts = new int[8]; // size 8, numbers 1-7
    for (RedSevenCard card : cards) {
      counts[card.getNumber()]++;
    }
    int maxRunLength = 0;
    int currentRunLength = 0;
    for (int count : counts) {
      if (count > 0) {
        currentRunLength++;
        if (currentRunLength > maxRunLength) {
          maxRunLength = currentRunLength;
        }
      } else {
        currentRunLength = 0;
      }
    }
    return maxRunLength;
  }

  /**
   * Determines if card card1 is higher than card2 based on the highest card rule.
   *
   * @param card1 the first card
   * @param card2 the second card
   * @return true if card1 is higher than card2
   */
  private static boolean isHigher(RedSevenCard card1, RedSevenCard card2) {
    if (card1.getNumber() > card2.getNumber()) {
      return true;
    } else if (card1.getNumber() == card2.getNumber()) {
      return card1.getColor().ordinal() < card2.getColor().ordinal();
    }
    return false;
  }
}

package cs3500.solored.view.hw02;

import java.io.IOException;

/**
 * Behaviors needed for a view of the RedSeven implementation
 * that transmits information to the user.
 */
public interface RedGameView {

  /**
   * Creates a String with state of the game.
   * This rendering includes
   * <ul>
   *   <li>The color of the card on the Canvas</li>
   *   <li>Each palette from P1 to Pn, where n is the number of palettes, where each palette
   *   has all of its card printed with one space between them</li>
   *   <li>A greater than symbol indicating the winning palette</li>
   *   <li>The hand, where all cards are printed with one space between them</li>
   * </ul>
   * An example below for a 4-palette, 7-hand game in-progress
   * Canvas: R
   * P1: R6 B1
   * > P2: R7
   * P3: V1
   * P4: I2
   * Hand: V2 I3 R1 O2 G6 R5 O1
   * @return
   */
  String toString();

  /**
   * Renders the model.
   * @throws IOException if the rendering fails for some reason.
   */
  void render() throws IOException;
}

package cs3500.solored.view.hw02;

import cs3500.solored.model.hw02.Color;
import cs3500.solored.model.hw02.RedGameModel;

import java.io.IOException;

/**
 * Implementation of the RedGameView interface for a game of Red7.
 */
public class SoloRedGameTextView implements RedGameView {
  private final RedGameModel<?> model;
  private final Appendable appendable;

  /**
   * Constructor for SoloRedGameTextView.
   *
   * @param model the RedGameModel to use for rendering.
   */
  public SoloRedGameTextView(RedGameModel<?> model) {
    this(model, System.out);

    if (this.appendable == null) {
      throw new IllegalArgumentException("Appendable cannot be null");
    }
  }

  /**
   * Constructor for SoloRedGameTextView with an Appendable.
   *
   * @param model      the RedGameModel to use for rendering.
   * @param appendable the Appendable object to append the output to.
   */
  public SoloRedGameTextView(RedGameModel<?> model, Appendable appendable) {
    if (appendable == null) {
      throw new IllegalArgumentException("Appendable cannot be null");
    }
    this.model = model;
    this.appendable = appendable;
  }

  @Override
  public void render() throws IOException {
    appendable.append(toString());
  }

  // i hate this thing
  // WHY IS IT DOING [[ ]] INSTEAD OF []
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    String canvasCardString = model.getCanvas().toString();
    Color canvasColor = parseColor(canvasCardString);
    sb.append("Canvas: ").append(canvasColor).append("\n");

    for (int i = 0; i < model.numPalettes(); i++) {
      if (i == model.winningPaletteIndex()) {
        sb.append("> ");
      }
      sb.append("P").append(i + 1).append(": ");
      for (int j = 0; j < model.getPalette(i).size(); j++) {
        sb.append(model.getPalette(i).get(j).toString());
        if (j < model.getPalette(i).size() - 1) {
          sb.append(" ");
        }
      }
      sb.append("\n");
    }
    sb.append("Hand: ");
    if (model.getHand().isEmpty()) {
      sb.append(" ");
    } else {
      for (int i = 0; i < model.getHand().size(); i++) {
        sb.append(model.getHand().get(i).toString());
        if (i < model.getHand().size() - 1) {
          sb.append(" ");
        }
      }
    }
    return sb.toString().trim();
  }



  /*
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    String canvasCardString = model.getCanvas().toString();
    Color canvasColor = parseColor(canvasCardString);
    sb.append("Canvas: ").append(canvasColor).append("\n");

    renderPalettes(sb);
    renderHand(sb);

    return sb.toString().trim();
  }


  // TODO: Remember to change back to private after testing
  public void renderHand(StringBuilder sb) {
    sb.append("Hand: ");
    if (model.getHand().isEmpty()) {
      sb.append(" ");
    } else {
      for (int i = 0; i < model.getHand().size(); i++) {
        sb.append(model.getHand().get(i).toString());
        if (i < model.getHand().size() - 1) {
          sb.append(" ");
        }
      }
    }
  }

  // TODO: Remember to change back to private after testing
  public void renderPalettes(StringBuilder sb) {
    for (int i = 0; i < model.numPalettes(); i++) {
      if (i == model.winningPaletteIndex()) {
        sb.append("> ");
      }
      sb.append("P").append(i + 1).append(": ");
      for (int j = 0; j < model.getPalette(i).size(); j++) {
        sb.append(model.getPalette(i).get(j).toString());
        if (j < model.getPalette(i).size() - 1) {
          sb.append(" ");
        }
      }
      sb.append("\n");
    }
  }
  */

  // TODO: Remember to change back to private after testing
  public Color parseColor(String cardString) {
    String colorChar = cardString.substring(0, 1);
    switch (colorChar) {
      case "R":
        return Color.RED;
      case "O":
        return Color.ORANGE;
      case "B":
        return Color.BLUE;
      case "I":
        return Color.INDIGO;
      case "V":
        return Color.VIOLET;
      default:
        throw new IllegalArgumentException("Unknown color: " + colorChar);
    }
  }

}

package cs3500.solored;

import cs3500.solored.controller.RedGameController;
import cs3500.solored.controller.SoloRedTextController;
import cs3500.solored.model.hw02.Color;
import cs3500.solored.model.hw02.RedGameModel;
import cs3500.solored.model.hw02.RedSevenCard;

import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Test class for the controller.
 */
public class ControllerTests {

  private RedGameModel<RedSevenCard> mockModel;
  private List<RedSevenCard> deck;
  private StringBuilder appendable;
  private RedGameController controller;

  @Before
  public void setUp() {
    deck = new ArrayList<>();
    for (Color color : Color.values()) {
      for (int number = 1; number <= 7; number++) {
        deck.add(new RedSevenCard(color, number));
      }
    }
    appendable = new StringBuilder();
    controller = new SoloRedTextController(new StringReader(""), appendable);
    // Mock Model Setup
    mockModel = new MockRedGameModel();
  }

  // Helper methods
  private void playGameWithInput(String input, RedGameModel<RedSevenCard> model) {
    controller = new SoloRedTextController(new StringReader(input), appendable);
    controller.playGame(model, deck, false, 4, 7);
  }

  private void playGameWithInput(String input) {
    playGameWithInput(input, this.mockModel);
  }


  class MockRedGameModel implements RedGameModel<RedSevenCard> {
    private boolean playToPaletteCalled = false;
    private boolean playToCanvasCalled = false;
    private boolean startGameCalled = false;

    @Override
    public void startGame(List<RedSevenCard> deck, boolean shuffle, int numPalettes, int handSize) {
      this.startGameCalled = true;
    }

    @Override
    public void playToPalette(int paletteIdx, int cardIdxInHand) {
      this.playToPaletteCalled = true;
    }

    @Override
    public void playToCanvas(int cardIdxInHand) {
      this.playToCanvasCalled = true;
    }

    @Override
    public void drawForHand() {
      // Do nothing for mock
    }

    @Override
    public int numOfCardsInDeck() {
      return 24;
    }

    @Override
    public int numPalettes() {
      return 4;
    }

    @Override
    public int winningPaletteIndex() {
      return 0;
    }

    @Override
    public boolean isGameOver() {
      return false;
    }

    @Override
    public boolean isGameWon() {
      return false;
    }

    @Override
    public List<RedSevenCard> getHand() {
      List<RedSevenCard> hand = new ArrayList<>();
      hand.add(new RedSevenCard(Color.RED, 1));
      return hand;
    }

    @Override
    public List<RedSevenCard> getPalette(int paletteNum) {
      List<RedSevenCard> palette = new ArrayList<>();
      palette.add(new RedSevenCard(Color.RED, 1));
      return palette;
    }

    @Override
    public RedSevenCard getCanvas() {
      return new RedSevenCard(Color.RED, 1);
    }

    @Override
    public List<RedSevenCard> getAllCards() {
      return deck;
    }

    // Add getters for checking if methods were called
    public boolean isPlayToPaletteCalled() {
      return playToPaletteCalled;
    }

    public boolean isPlayToCanvasCalled() {
      return playToCanvasCalled;
    }

    public boolean isStartGameCalled() {
      return startGameCalled;
    }
  }

  /*
  @Test
  public void testPlayGameValidInput() throws IOException {
    playGameWithInput("palette 1 1 canvas 2 q");
    String expectedOutput = "Canvas: R1\n" +
            "P1: R1 >\n" +
            "P2: R1 \n" +
            "P3: R1 \n" +
            "P4: R1 \n" +
            "Hand: R1 \n" +
            "Number of cards in deck: 24\n" +
            "Canvas: R1\n" +
            "P1: R1 >\n" +
            "P2: R1 \n" +
            "P3: R1 \n" +
            "P4: R1 \n" +
            "Hand: R1 \n" +
            "Number of cards in deck: 24\n" +
            "Game quit!\n" +
            "State of game when quit:\n" +
            "Canvas: R1\n" +
            "P1: R1 >\n" +
            "P2: R1 \n" +
            "P3: R1 \n" +
            "P4: R1 \n" +
            "Hand: R1 \n" +
            "Number of cards in deck: 24\n";
    assertEquals(expectedOutput, appendable.toString());
  }
  */

  /**
   * FAILS.
   */
  @Test
  public void testPlayGameInvalidCommand() {
    playGameWithInput("invalidCommand q");
    String expectedOutput = "Invalid command. Try again. Valid commands are: palette, canvas, q\n" +
            "Game quit!\n" +
            "State of game when quit:\n" +
            "Canvas: R1\n" +
            "P1: R1 >\n" +
            "P2: R1 \n" +
            "P3: R1 \n" +
            "P4: R1 \n" +
            "Hand: R1 \n" +
            "Number of cards in deck: 24\n";
    assertEquals(expectedOutput, appendable.toString());
  }

  /**
   * FAILS.
   */
  @Test
  public void testPlayGameInvalidMove() {
    RedGameModel<RedSevenCard> invalidMoveModel = new MockRedGameModel() {
      @Override
      public void playToPalette(int paletteIdx, int cardIdxInHand) {
        throw new IllegalArgumentException("Invalid card index in hand");
      }
    };
    playGameWithInput("palette 1 8 q", invalidMoveModel); // Try to play a card out of bounds
    String expectedOutput = "Canvas: R1\n" +
            "P1: R1 >\n" +
            "P2: R1 \n" +
            "P3: R1 \n" +
            "P4: R1 \n" +
            "Hand: R1 \n" +
            "Number of cards in deck: 24\n" +
            "Invalid input. Please enter an integer or 'q' to quit.\n" +
            "Invalid move. Try again. Invalid card index in hand\n" +
            "Game quit!\n" +
            "State of game when quit:\n" +
            "Canvas: R1\n" +
            "P1: R1 >\n" +
            "P2: R1 \n" +
            "P3: R1 \n" +
            "P4: R1 \n" +
            "Hand: R1 \n" +
            "Number of cards in deck: 24\n";
    assertEquals(expectedOutput, appendable.toString());
  }

  /**
   FAILS.
   */

  @Test
  public void testPlayGameQuit() {
    playGameWithInput("q");
    String expectedOutput = "Canvas: R1\n" +
            "P1: R1 >\n" +
            "P2: R1 \n" +
            "P3: R1 \n" +
            "P4: R1 \n" +
            "Hand: R1 \n" +
            "Number of cards in deck: 24\n" +
            "Game quit!\n" +
            "State of game when quit:\n" +
            "Canvas: R1\n" +
            "P1: R1 >\n" +
            "P2: R1 \n" +
            "P3: R1 \n" +
            "P4: R1 \n" +
            "Hand: R1 \n" +
            "Number of cards in deck: 24\n";
    assertEquals(expectedOutput, appendable.toString());
  }

  @Test
  public void testPlayGameGameOverWin() {
    RedGameModel<RedSevenCard> winningModel = new MockRedGameModel() {
      private int count = 0;

      @Override
      public boolean isGameOver() {
        // Simulate game over after 4 moves
        count++;
        return count > 4;
      }

      @Override
      public boolean isGameWon() {
        return true;
      }
    };
    playGameWithInput("palette 1 1 palette 2 2 palette 3 3 palette 4 4", winningModel);
    assertTrue(appendable.toString().contains("Game won."));
  }

  @Test
  public void testPlayGameGameOverLose() throws IOException {
    RedGameModel<RedSevenCard> losingModel = new MockRedGameModel() {
      @Override
      public boolean isGameOver() {
        return true; // Simulate game over immediately
      }
    };
    playGameWithInput("palette 2 1", losingModel);
    assertTrue(appendable.toString().contains("Game lost."));
  }

  @Test
  public void testStartGameThrowsException() {
    RedGameModel<RedSevenCard> throwingModel = new MockRedGameModel() {
      @Override
      public void startGame(List<RedSevenCard> deck, boolean shuffle,
                            int numPalettes, int handSize) {
        throw new IllegalArgumentException("Invalid game setup.");
      }
    };

    try {
      playGameWithInput("q", throwingModel);
      fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException e) {
      assertEquals("Game cannot be started: Invalid game setup.", e.getMessage());
    }
  }

  // Existing constructor tests...

  @Test(expected = IllegalStateException.class)
  public void testPlayGameWithIOException() {
    // Mock Appendable that throws IOException
    Appendable mockAppendable = new Appendable() {
      @Override
      public Appendable append(CharSequence csq) throws IOException {
        throw new IOException("Simulated IO Exception");
      }

      @Override
      public Appendable append(CharSequence csq, int start, int end) throws IOException {
        throw new IOException("Simulated IO Exception");
      }

      @Override
      public Appendable append(char c) throws IOException {
        throw new IOException("Simulated IO Exception");
      }
    };

    controller = new SoloRedTextController(new StringReader(""), mockAppendable);
    controller.playGame(mockModel, deck, false, 4, 7);
  }
}

package cs3500.solored;

import org.junit.Before;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;

import cs3500.solored.model.hw02.Color;
import cs3500.solored.model.hw02.RedSevenCard;
import cs3500.solored.model.hw02.SoloRedGameModel;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;

/**
 * Testing class.
 */
public class ModelTests {

  private SoloRedGameModel model;
  private List<RedSevenCard> deck;

  @Before
  public void setUp() {
    model = new SoloRedGameModel();
    deck = new ArrayList<>();
    for (Color color : Color.values()) {
      for (int number = 1; number <= 7; number++) {
        deck.add(new RedSevenCard(color, number));
      }
    }
  }

  // Card Tests
  @Test
  public void testGetAllCards() {
    List<RedSevenCard> allCards = model.getAllCards();
    assertEquals(35, allCards.size()); // 5 colors * 7 numbers
  }

  @Test
  public void testGetAllCardsOrder() {
    List<RedSevenCard> allCards = model.getAllCards();
    assertEquals(35, allCards.size());

    List<RedSevenCard> sortedCards = new ArrayList<>(allCards);
    sortedCards.sort(new CardComparator());

    // Check if the orig. list is in the same order as sorted list
    for (int i = 0; i < allCards.size(); i++) {
      assertEquals("Card at index " + i + " is not in the correct order",
              sortedCards.get(i).toString(), allCards.get(i).toString());
    }
  }

  private static class CardComparator implements Comparator<RedSevenCard> {
    @Override
    public int compare(RedSevenCard card1, RedSevenCard card2) {
      // Compare by color then number
      int colorComparison = card1.getColor().ordinal() - card2.getColor().ordinal();
      if (colorComparison != 0) {
        return colorComparison;
      }
      // If colors are ==, compare by number
      return card1.getNumber() - card2.getNumber();
    }
  }

  // Deck Tests
  @Test
  public void testShuffle() {
    Random random = new Random(12345);
    model = new SoloRedGameModel(random);
    model.startGame(deck, true, 4, 7);
    List<RedSevenCard> initialDeck = new ArrayList<>(deck);
    Collections.shuffle(initialDeck);
    // Verify that the deck is shuffled
    assertNotEquals(initialDeck, model.getAllCards());
  }

  @Test
  public void testShuffleWithDifferentRandomSeeds() {
    model = new SoloRedGameModel(new Random(1));
    model.startGame(deck, true, 4, 7);
    List<RedSevenCard> shuffledDeck1 = model.getAllCards();

    model = new SoloRedGameModel(new Random(2));
    model.startGame(deck, true, 4, 7);
    List<RedSevenCard> shuffledDeck2 = model.getAllCards();

    assertNotEquals(shuffledDeck1, shuffledDeck2);
  }

  @Test
  public void testShuffleWithSameRandomSeed() {
    Random random = new Random(12345);
    model = new SoloRedGameModel(random);
    model.startGame(deck, true, 4, 7);
    List<RedSevenCard> shuffledDeck1 = model.getAllCards();

    model = new SoloRedGameModel(random);
    model.startGame(deck, true, 4, 7);
    List<RedSevenCard> shuffledDeck2 = model.getAllCards();

    assertEquals(shuffledDeck1, shuffledDeck2);
  }

  @Test
  public void testShuffleWithNullRandom() {
    try {
      model = new SoloRedGameModel(null);
      fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException e) {
      // Expected
    }
  }

  @Test
  public void testNumOfCardsInDeck() {
    model.startGame(deck, false, 4, 7);
    assertEquals(deck.size() - 4 - 7, model.numOfCardsInDeck());
  }

  // Palette Tests
  @Test
  public void testNumPalettes() {
    model.startGame(deck, false, 4, 7);
    assertEquals(4, model.numPalettes());
  }

  @Test
  public void testGetPalette() {
    model.startGame(deck, false, 4, 7);
    List<RedSevenCard> palette = model.getPalette(0);
    assertEquals(1, palette.size());
  }

  @Test
  public void testGetPaletteWithInvalidIndex() {
    model.startGame(deck, false, 4, 7);
    try {
      model.getPalette(-1);
      fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException e) {
      // Expected
    }
  }

  @Test
  public void testPlayToPalette() {
    model.startGame(deck, false, 4, 7);
    model.playToPalette(0, 0);
    assertEquals(7, model.getHand().size());
    assertEquals(2, model.getPalette(0).size());
  }

  @Test(expected = IllegalStateException.class)
  public void testPlayToPaletteGameNotStarted() {
    model.playToPalette(0, 0);
  }

  @Test
  public void testPlayToPaletteWithInvalidIndex() {
    model.startGame(deck, false, 4, 7);
    try {
      model.playToPalette(-1, 0);
      fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException e) {
      // Expected
    }
  }

  @Test
  public void testPlayToPaletteWithWinningPalette() {
    model.startGame(deck, false, 4, 7);
    int winningIndex = model.winningPaletteIndex();
    try {
      model.playToPalette(winningIndex, 0);
      fail("Expected IllegalStateException");
    } catch (IllegalStateException e) {
      // Expected
    }
  }

  // Canvas Tests
  @Test
  public void testPlayToCanvas() {
    model.startGame(deck, false, 4, 7);
    model.playToCanvas(0);
    // It draws at the end of playToCanvas so handSize should not change.
    assertEquals(7, model.getHand().size());
    assertEquals(Color.RED, model.getCanvas().getColor());
  }

  @Test(expected = IllegalStateException.class)
  public void testPlayToCanvasGameNotStarted() {
    model.playToCanvas(0);
  }

  @Test
  public void testPlayToCanvasWithInvalidIndex() {
    model.startGame(deck, false, 4, 7);
    try {
      model.playToCanvas(-1);
      fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException e) {
      // Expected
    }
  }

  @Test(expected = IllegalStateException.class)
  public void testPlayToCanvasWithOneCard() {
    model.startGame(deck, false, 4, 7);
    while (model.getHand().size() > 1) {
      model.playToPalette(0, 0);
    }
    model.playToCanvas(0); // Should throw IllegalStateException
  }

  @Test
  public void testGetCanvas() {
    model.startGame(deck, false, 4, 7);
    RedSevenCard canvasCard = model.getCanvas();
    assertEquals(Color.RED, canvasCard.getColor());
    assertEquals(7, canvasCard.getNumber());
  }

  @Test
  public void testGetCanvasWhenGameNotStarted() {
    try {
      model.getCanvas();
      fail("Expected IllegalStateException");
    } catch (IllegalStateException e) {
      // Expected
    }
  }

  // Hand Tests
  @Test
  public void testDrawForHand() {
    model.startGame(deck, false, 4, 7);
    model.drawForHand();
    assertEquals(7, model.getHand().size());
  }

  @Test(expected = IllegalStateException.class)
  public void testDrawForHandGameNotStarted() {
    model.drawForHand();
  }

  @Test
  public void testDrawForHandWithFullHand() {
    model.startGame(deck, false, 4, 7);
    model.drawForHand();
    assertEquals(7, model.getHand().size());
  }

  @Test
  public void testGetHand() {
    model.startGame(deck, false, 4, 7);
    assertEquals(7, model.getHand().size());
  }

  // Game Tests
  @Test
  public void testStartGame() {
    model.startGame(deck, false, 4, 7);
    assertEquals(4, model.numPalettes());
    assertEquals(7, model.getHand().size());
    assertFalse(model.isGameOver());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testStartGameInvalidPalettes() {
    model.startGame(deck, false, 1, 7);
  }

  @Test(expected = IllegalArgumentException.class)
  public void testStartGameInvalidHandSize() {
    model.startGame(deck, false, 4, 0);
  }

  @Test
  public void testStartGameWithShuffle() {
    model.startGame(deck, true, 4, 7);
    List<RedSevenCard> initialDeck = new ArrayList<>(deck);
    Collections.shuffle(initialDeck);
    assertNotEquals(initialDeck, model.getAllCards());
  }

  @Test
  public void testStartGameWithoutShuffle() {
    model.startGame(deck, false, 4, 7);
    List<RedSevenCard> initialDeck = new ArrayList<>(deck);
    assertEquals(initialDeck, model.getAllCards());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testStartGameWithInvalidHandSize() {
    model.startGame(deck, false, 4, -1);
  }

  @Test(expected = IllegalArgumentException.class)
  public void testStartGameWithInvalidPaletteCount() {
    model.startGame(deck, false, 1, 7);
  }

  @Test
  public void testIsGameOver() {
    model.startGame(deck, false, 4, 7);
    assertFalse(model.isGameOver());
  }

  @Test
  public void testIsGameOverWhenPaletteDoesNotWin() {
    model.startGame(deck, false, 4, 7);
    model.playToPalette(0, 0);
    assertFalse(model.isGameOver());
  }

  @Test
  public void testIsGameOverWhenGameIsNotOver() {
    model.startGame(deck, false, 4, 7);
    assertFalse(model.isGameOver());
  }

  @Test
  public void testIsGameWonWhenGameIsNotOver() {
    model.startGame(deck, false, 4, 7);
    try {
      model.isGameWon();
      fail("Expected IllegalStateException");
    } catch (IllegalStateException e) {
      // Expected
    }
  }

  @Test
  public void testPlayToPaletteWhenGameNotStarted() {
    try {
      model.playToPalette(0, 0);
      fail("Expected IllegalStateException");
    } catch (IllegalStateException e) {
      // Expected
    }
  }

  @Test
  public void testPlayToCanvasWhenGameNotStarted() {
    try {
      model.playToCanvas(0);
      fail("Expected IllegalStateException");
    } catch (IllegalStateException e) {
      // Expected
    }
  }

  @Test
  public void testDrawForHandWhenGameNotStarted() {
    try {
      model.drawForHand();
      fail("Expected IllegalStateException");
    } catch (IllegalStateException e) {
      // Expected
    }
  }

  @Test
  public void testGetHandDefensiveCopy() {
    model.startGame(deck, false, 4, 7);
    List<RedSevenCard> handCopy = model.getHand();
    handCopy.remove(0); // Modify the copy
    assertEquals(7, model.getHand().size()); // Original hand should be unchanged
  }

  @Test
  public void testGetPaletteDefensiveCopy() {
    model.startGame(deck, false, 4, 7);
    List<RedSevenCard> paletteCopy = model.getPalette(0);
    paletteCopy.remove(0); // Modify the copy
    assertEquals(1, model.getPalette(0).size()); // Original palette should be unchanged
  }

  @Test(expected = IllegalStateException.class)
  public void testStartGameWhenGameInProgress() {
    model.startGame(deck, false, 4, 7);
    model.startGame(deck, false, 4, 7); // Should throw an exception
  }
}
Error:
Failed: testDrawHandBehavior(cs3500.solored.HW02PublicTests)

Message: Checking number of cards in hand after playing 1 card expected:<3> but was:<4>

Stack:
java.lang.AssertionError: Checking number of cards in hand after playing 1 card expected:<3> but was:<4>
    at org.junit.Assert.fail(Assert.java:89)
    at org.junit.Assert.failNotEquals(Assert.java:835)
    at org.junit.Assert.assertEquals(Assert.java:647)
    at cs3500.solored.utils.InstructorUtils.assertDrawHandBehaviorWithPlayToPalette(InstructorUtils.java:459)
    at cs3500.solored.HW02PublicTests.testDrawHandBehavior(HW02PublicTests.java:120)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at edu.neu.TAPRunner$ObserveTestTarget.evaluate(TAPRunner.java:180)
    at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:299)
    at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:293)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.lang.Thread.run(Thread.java:829)