diff --git a/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/Game.java b/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/Game.java index eec2ba8..4c0c68a 100644 --- a/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/Game.java +++ b/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/Game.java @@ -3,23 +3,80 @@ import lombok.Getter; +import java.util.List; +import java.util.Map; + public class Game { @Getter - private Player p1; + private final Player p1; @Getter - private Player p2; - + private final Player p2; @Getter private Player nextTurn; + @Getter + private final String[][] gameState = new String[3][3]; - private String[][] gameState = new String[3][3]; + private int emptyBoxes; - public Game(String p1Char, String p2Char) { - p1 = new Player(p1Char); - p2 = new Player(p2Char); + private static final List topRowBoxes = List.of(1, 2, 3); + private static final List midRowBoxes = List.of(4, 5, 6); + private static final List bottomRowBoxes = List.of(7, 8, 9); + private static final List leftColBoxes = List.of(1, 4, 7); + private static final List midColBoxes = List.of(2, 5, 8); + private static final List rightColBoxes = List.of(3, 6, 9); + private static final List leftDiagonalBoxes = List.of(1, 5, 9); + private static final List rightDiagonalBoxes = List.of(3, 5, 7); + + private static final Map>> boxToWinningLines = Map.ofEntries( + Map.entry(1, List.of(topRowBoxes, leftColBoxes, leftDiagonalBoxes)), + Map.entry(2, List.of(topRowBoxes, midColBoxes)), + Map.entry(3, List.of(topRowBoxes, rightColBoxes, rightDiagonalBoxes)), + Map.entry(4, List.of(midRowBoxes, leftColBoxes)), + Map.entry(5, List.of(midRowBoxes, midColBoxes, leftDiagonalBoxes, rightDiagonalBoxes)), + Map.entry(6, List.of(midRowBoxes, rightColBoxes)), + Map.entry(7, List.of(bottomRowBoxes, leftColBoxes, rightDiagonalBoxes)), + Map.entry(8, List.of(bottomRowBoxes, midColBoxes)), + Map.entry(9, List.of(bottomRowBoxes, rightColBoxes, leftDiagonalBoxes)) + ); + public Game(String p1Char, String p2Char) { + this.p1 = new Player(p1Char); + this.p2 = new Player(p2Char); // init next turn for player 1 - nextTurn = p1; + this.nextTurn = p1; + this.emptyBoxes = 9; + } + + public void playGame() { + Player victor = null; + + while (!isGameOverByExhaustionOfBoxes()) { + IOHelper.printGameState(getGameState()); + Player nextTurnPlayer = getNextTurn(); + IOHelper.printNextTurn(nextTurnPlayer); + int boxNumber = IOHelper.getBoxNumberToBeFilled(); + while (!fillBoxSuccess(boxNumber)) { + System.out.println("Invalid Box number. Please try again.\n"); + IOHelper.printNextTurn(getNextTurn()); + boxNumber = IOHelper.getBoxNumberToBeFilled(); + } + victor = checkVictory(boxNumber, nextTurnPlayer); + if (victor != null) { + break; + } + } + + IOHelper.printGameState(getGameState()); + IOHelper.declareVictor(victor); + } + + public boolean fillBoxSuccess(int boxNumber) { + try { + nextAttempt(boxNumber); + return true; + } catch (IllegalArgumentException | IllegalStateException e) { + return false; + } } public String getCharInBox(int box) { @@ -41,8 +98,10 @@ public void nextAttempt(int box) { if (box < 1 || box > 9) throw new IllegalArgumentException("Box no. must be between 1 and 9"); if (gameState[row][col] != null) throw new IllegalStateException("This box is not empty"); + if (isGameOverByExhaustionOfBoxes()) throw new IllegalStateException("All boxes have been filled. Game over."); - gameState[row][col] = nextTurn.getCharacter(); + gameState[row][col] = getNextTurn().getCharacter(); + emptyBoxes -= 1; // switch turn of players if (nextTurn == p1) nextTurn = p2; @@ -50,20 +109,42 @@ public void nextAttempt(int box) { } /** - * Checks board state and tells if any winners + * Checks if the game has been won by a player * - * @return p1 or p2 whoever has won; or null if no winner yet + * @param lastFilledBox Box number that was last filled correctly + * @param playerWhoLastPlayed The player who played the last move + * @return The player who has won the game, null if no one has won yet */ - public Player checkVictory() { - // TODO + public Player checkVictory(int lastFilledBox, Player playerWhoLastPlayed) { + + if (!playerWhoLastPlayed.getCharacter().equals(getCharInBox(lastFilledBox))) { + throw new IllegalStateException("Filled character does not match player's marking character"); + } + String playerCharacter = playerWhoLastPlayed.getCharacter(); + + for (List possibleWinningLine : boxToWinningLines.get(lastFilledBox)) { + + // init number of character matches for this player to 1 -- for the currently filled box + int lineMatches = 0; + + // check every line to see a possible complete line made for this box + for (int boxNo : possibleWinningLine) { + if (playerCharacter.equals(getCharInBox(boxNo))) { + lineMatches += 1; + continue; + } + break; + } + + // check if this current line is complete with the character for this player + if (lineMatches == 3) { + return playerWhoLastPlayed; + } + } return null; } - public String printGameState() { - return " " + gameState[0][0] + " | " + gameState[0][1] + " | " + gameState[0][2] + "\n" + - "------------\n" + - " " + gameState[1][0] + " | " + gameState[1][1] + " | " + gameState[1][2] + "\n" + - "------------\n" + - " " + gameState[2][0] + " | " + gameState[2][1] + " | " + gameState[2][2]; + public boolean isGameOverByExhaustionOfBoxes() { + return emptyBoxes == 0; } } diff --git a/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/IOHelper.java b/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/IOHelper.java new file mode 100644 index 0000000..0b521e0 --- /dev/null +++ b/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/IOHelper.java @@ -0,0 +1,87 @@ +package com.scaler.tictactoe; + +import java.io.BufferedReader; +import java.io.Console; +import java.io.IOException; +import java.io.InputStreamReader; + +public class IOHelper { + + private static String readLine() { + Console console = System.console(); + if (console != null) { + return console.readLine(); + } else { + BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in)); + try { + return consoleReader.readLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + static void declareVictor(Player victor) { + if (victor == null) { + System.out.println("This game has ended in a draw"); + } else { + System.out.println("Player " + victor.getCharacter() + " has won! 🎊 🍾"); + } + } + + static int getBoxNumberToBeFilled() { + System.out.println("Enter box number (1-9) to be filled."); + int boxNumber; + try { + boxNumber = Integer.parseInt(readLine()); + System.out.println(); + if (boxNumber < 1 || boxNumber > 9) { + System.out.println("Invalid Box number. Please try again.\n"); + return getBoxNumberToBeFilled(); + } + } catch (NumberFormatException e) { + System.out.println("Invalid Box number. Please try again.\n"); + return getBoxNumberToBeFilled(); + } + return boxNumber; + } + + static void printNextTurn(Player nextTurn) { + System.out.println("Player " + nextTurn.getCharacter() + " to play."); + } + + static void printGameState(final String[][] gameState) { + System.out.println("Current Game State : \n"); + for (String[] row : gameState) { + for (String col : row) { + if (col == null) + col = "-"; + System.out.print(col + " "); + } + System.out.println(); + } + System.out.println(); + } + + static String getPlayerString(int playerNum) { + System.out.println("Enter character for Player " + playerNum); + String character = readLine(); + System.out.println(); + while (character.length() > 1) { + System.out.println("Please enter a single character."); + character = readLine(); + System.out.println(); + } + return character; + } + + static String getPlayerStringNotEqualTo(int playerNum, String character) { + String newChar = getPlayerString(playerNum); + while (newChar.equals(character)) { + System.out.println("Character cannot be same as " + character); + System.out.println(); + newChar = getPlayerString(playerNum); + } + return newChar; + } +} diff --git a/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/Main.java b/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/Main.java index 55a0864..0b1ba7c 100644 --- a/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/Main.java +++ b/tic-tac-toe-cli/src/main/java/com/scaler/tictactoe/Main.java @@ -1,21 +1,25 @@ package com.scaler.tictactoe; public class Main { - public static void main(String[] args) { - - Game game = new Game("X", "O"); - System.out.println(game.printGameState()); + public static void main(String[] args) { /* - TODO: Create the entire game; steps are: - 1. Construct game object with 2 player characters - 2. For every turn, print whose turn it is, and state of game (3x3 box) - 3. Read input (between 1-9) as the box to be marked by next player + Create entire game. Steps: + 1. Construct game object with 2 players + 2. For every turn, print whose turn it is, with the state + 3. Read input (1-9) as the box to be marked 4. Validate input and mark the box - 4.1 If input invalid, make player enter box no again - 5. Repeat steps 2-4 until either; - 5.1 checkVictory function shows any player has won - 5.2 all boxes have been marked + 4.1. If the input is invalid, make the player input the box again + 5. Repeat steps 2-4 until + 5.1. checkVictory function shows if any player has won + 5.2. all boxes have been marked */ + + String p1Char = IOHelper.getPlayerString(1); + String p2Char = IOHelper.getPlayerStringNotEqualTo(2, p1Char); + Game game = new Game(p1Char, p2Char); + game.playGame(); + System.out.println("Thanks for playing! 🎊🎊🎊"); } + } diff --git a/tic-tac-toe-cli/src/test/java/com/scaler/tictactoe/GameTests.java b/tic-tac-toe-cli/src/test/java/com/scaler/tictactoe/GameTests.java index 56fce40..7c5ded6 100644 --- a/tic-tac-toe-cli/src/test/java/com/scaler/tictactoe/GameTests.java +++ b/tic-tac-toe-cli/src/test/java/com/scaler/tictactoe/GameTests.java @@ -1,7 +1,6 @@ package com.scaler.tictactoe; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; import static org.junit.jupiter.api.Assertions.*; @@ -38,9 +37,7 @@ void canMarkBoxesViaAttempts() { // check that already marked boxes are not allowed to be marked - assertThrowsExactly(IllegalStateException.class, () -> { - g.nextAttempt(1); - }); + assertThrowsExactly(IllegalStateException.class, () -> g.nextAttempt(1)); } @@ -48,9 +45,40 @@ void canMarkBoxesViaAttempts() { void throwsExceptionForInvalidBoxAttempt() { Game g = new Game("❌", "⭕️"); - assertThrowsExactly(IllegalArgumentException.class, () -> { - g.nextAttempt(10); - }); + assertThrowsExactly(IllegalArgumentException.class, () -> g.nextAttempt(10)); } + + @Test + void checkVictoryTest() { + Game game = new Game("X", "O"); + + // Player 1 marks box 1 + game.nextAttempt(1); + // Player 2 plays box 5 + Player p = game.getNextTurn(); + game.nextAttempt(5); + assertEquals(game.getP2(), p); // Player 1 should have the next turn + // Game has only been played by two moves. No winner yet + assertNull(game.checkVictory(5, p)); + + p = game.getNextTurn(); + assertEquals(game.getP1(), p); // Player 1 should have the next turn + game.nextAttempt(2); + // box number 4 has not been filled + Player finalP = p; + assertThrowsExactly(IllegalStateException.class, () -> game.checkVictory(4, finalP)); + // box number 5 was filled by player 2 and not player 1 + assertThrowsExactly(IllegalStateException.class, () -> game.checkVictory(5, finalP)); + // Game has only been played by three total moves. No winner yet + assertNull(game.checkVictory(2, p)); + + // Player 2 + game.nextAttempt(6); + // Player 1 has played boxes 1,2,3 + p = game.getNextTurn(); + assertEquals(game.getP1(), p); // Player 1 should have the next turn + game.nextAttempt(3); + assertEquals(game.getP1(), game.checkVictory(3, p)); + } } diff --git a/tic-tac-toe-cli/src/test/java/com/scaler/tictactoe/PlayerTests.java b/tic-tac-toe-cli/src/test/java/com/scaler/tictactoe/PlayerTests.java index 2e0a681..dfb1160 100644 --- a/tic-tac-toe-cli/src/test/java/com/scaler/tictactoe/PlayerTests.java +++ b/tic-tac-toe-cli/src/test/java/com/scaler/tictactoe/PlayerTests.java @@ -7,7 +7,7 @@ public class PlayerTests { @Test - void constructPlayerWithCharacter () { + void constructPlayerWithCharacter() { Player p1 = new Player("❌"); assertEquals("❌", p1.getCharacter()); }