diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..a1757ae --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..69ace3f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ce31086 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CodeKatas_20240507_CodeSmells.iml b/CodeKatas_20240507_CodeSmells.iml new file mode 100644 index 0000000..7c56e11 --- /dev/null +++ b/CodeKatas_20240507_CodeSmells.iml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/CodeSmells.iml b/java/CodeSmells.iml index d0c7dd5..f3fdec3 100644 --- a/java/CodeSmells.iml +++ b/java/CodeSmells.iml @@ -10,5 +10,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/src/Board.java b/java/src/Board.java index 014a39d..88500b6 100644 --- a/java/src/Board.java +++ b/java/src/Board.java @@ -1,42 +1,45 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; -public class Board -{ - private List _plays = new ArrayList<>(); +public class Board { + private final List plays = new ArrayList<>(); - public Board() - { - for (int i = 0; i < 3; i++) - { - for (int j = 0; j < 3; j++) - { - Tile tile = new Tile(); - tile.X = i; - tile.Y = j; - tile.Symbol = ' '; - _plays.add(tile); + public Board() { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + Tile tile = new Tile(i, j); + plays.add(tile); } } } - public Tile TileAt(int x, int y) - { - for (Tile t : _plays) { - if (t.X == x && t.Y == y){ - return t; + public Symbol getSymbolAt(int x, int y) { + return Optional.ofNullable(this.tileAt(x, y)).map(Tile::getSymbol).orElse(null); + } + + public void setSymbolAt(int x, int y, Symbol symbol) { + Optional.ofNullable(this.tileAt(x, y)).ifPresent(t -> t.setSymbol(symbol)); + } + + public String print() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + Tile currentTile = this.tileAt(i, j); + sb.append(Optional.ofNullable(currentTile).map(Tile::getSymbol).map(Symbol::getAsChar).orElse(' ')); } + sb.append("\n"); } - return null; + return sb.toString(); } - public void AddTileAt(char symbol, int x, int y) - { - Tile newTile = new Tile(); - newTile.X = x; - newTile.Y = y; - newTile.Symbol = symbol; - - TileAt(x,y).Symbol = symbol; + private Tile tileAt(int x, int y) { + for (Tile t : plays) { + if (t.getX() == x && t.getY() == y) { + return t; + } + } + return null; } -} \ No newline at end of file +} diff --git a/java/src/Game.java b/java/src/Game.java index bf4c95d..f52ef2b 100644 --- a/java/src/Game.java +++ b/java/src/Game.java @@ -1,69 +1,70 @@ +import java.util.Optional; + public class Game { - private char _lastSymbol = ' '; - private Board _board = new Board(); + private Symbol lastSymbol = null; + private final Board board = new Board(); + + public void play(char rawSymbol, int x, int y) { + Symbol symbol = Symbol.fromChar(rawSymbol); - public void Play(char symbol, int x, int y) throws Exception { //if first move - if (_lastSymbol == ' ') { - //if player is X - if (symbol == 'O') { - throw new Exception("Invalid first player"); - } + if (lastSymbol == null && symbol == Symbol.O) { + throw new IllegalArgumentException("Invalid first player"); } + //if not first move but player repeated - else if (symbol == _lastSymbol) { - throw new Exception("Invalid next player"); + else if (symbol.equals(lastSymbol)) { + throw new IllegalArgumentException("Invalid next player"); } + //if not first move but play on an already played tile - else if (_board.TileAt(x, y).Symbol != ' ') { - throw new Exception("Invalid position"); + else if (board.getSymbolAt(x, y) != null) { + throw new IllegalArgumentException("Invalid position"); } // update game state - _lastSymbol = symbol; - _board.AddTileAt(symbol, x, y); + lastSymbol = symbol; + board.setSymbolAt(x, y, symbol); } - public char Winner() { + public Optional computeWinner() { //if the positions in first row are taken - if (_board.TileAt(0, 0).Symbol != ' ' && - _board.TileAt(0, 1).Symbol != ' ' && - _board.TileAt(0, 2).Symbol != ' ') { + if (board.getSymbolAt(0, 0) != null && + board.getSymbolAt(0, 1) != null && + board.getSymbolAt(0, 2) != null) { //if first row is full with same symbol - if (_board.TileAt(0, 0).Symbol == - _board.TileAt(0, 1).Symbol && - _board.TileAt(0, 2).Symbol == _board.TileAt(0, 1).Symbol) { - return _board.TileAt(0, 0).Symbol; + if (board.getSymbolAt(0, 0).equals(board.getSymbolAt(0, 1)) && + board.getSymbolAt(0, 2).equals(board.getSymbolAt(0, 1))) { + return Optional.ofNullable(board.getSymbolAt(0, 0)); } } //if the positions in first row are taken - if (_board.TileAt(1, 0).Symbol != ' ' && - _board.TileAt(1, 1).Symbol != ' ' && - _board.TileAt(1, 2).Symbol != ' ') { + if (board.getSymbolAt(1, 0) != null && + board.getSymbolAt(1, 1) != null && + board.getSymbolAt(1, 2) != null) { //if middle row is full with same symbol - if (_board.TileAt(1, 0).Symbol == - _board.TileAt(1, 1).Symbol && - _board.TileAt(1, 2).Symbol == - _board.TileAt(1, 1).Symbol) { - return _board.TileAt(1, 0).Symbol; + if (board.getSymbolAt(1, 0).equals(board.getSymbolAt(1, 1)) && + board.getSymbolAt(1, 2).equals(board.getSymbolAt(1, 1))) { + return Optional.ofNullable(board.getSymbolAt(1, 0)); } } //if the positions in first row are taken - if (_board.TileAt(2, 0).Symbol != ' ' && - _board.TileAt(2, 1).Symbol != ' ' && - _board.TileAt(2, 2).Symbol != ' ') { + if (board.getSymbolAt(2, 0) != null && + board.getSymbolAt(2, 1) != null && + board.getSymbolAt(2, 2) != null) { //if middle row is full with same symbol - if (_board.TileAt(2, 0).Symbol == - _board.TileAt(2, 1).Symbol && - _board.TileAt(2, 2).Symbol == - _board.TileAt(2, 1).Symbol) { - return _board.TileAt(2, 0).Symbol; + if (board.getSymbolAt(2, 0).equals(board.getSymbolAt(2, 1)) && + board.getSymbolAt(2, 2).equals(board.getSymbolAt(2, 1))) { + return Optional.ofNullable(board.getSymbolAt(2, 0)); } } - return ' '; + return Optional.empty(); + } + + public String printBoard() { + return board.print(); } } - diff --git a/java/src/Symbol.java b/java/src/Symbol.java new file mode 100644 index 0000000..873833a --- /dev/null +++ b/java/src/Symbol.java @@ -0,0 +1,20 @@ +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@AllArgsConstructor +@Getter +public enum Symbol { + X('X'), + O('O'); + + private final char asChar; + + public static Symbol fromChar(char in) { + return Arrays.stream(Symbol.values()) + .filter(s -> s.asChar == Character.toUpperCase(in)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid symbol: %s".formatted(in))); + } +} diff --git a/java/src/Tile.java b/java/src/Tile.java index 03be347..378a666 100644 --- a/java/src/Tile.java +++ b/java/src/Tile.java @@ -1,7 +1,12 @@ +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; -public class Tile -{ - public int X; - public int Y; - public char Symbol; -} \ No newline at end of file +@Getter +@RequiredArgsConstructor +public class Tile { + private final int x; + private final int y; + @Setter + private Symbol symbol; +} diff --git a/java/test/GameTest.java b/java/test/GameTest.java new file mode 100644 index 0000000..c1da8f6 --- /dev/null +++ b/java/test/GameTest.java @@ -0,0 +1,206 @@ +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GameTest { + private Game game; + + @BeforeEach + void InitializeGame() { + game = new Game(); + } + + @Test + void NotAllowPlayerOToPlayFirst() { + assertThrows(IllegalArgumentException.class, () -> game.play('O', 0, 0)); + } + + @Test + void NotAllowPlayerXToPlayTwiceInARow() { + assertThrows(IllegalArgumentException.class, () -> { + game.play('X', 0, 0); + game.play('X', 1, 0); + }); + } + + @Test + void NotAllowPlayerToPlayInLastPlayedPosition() { + assertThrows(IllegalArgumentException.class, () -> { + game.play('X', 0, 0); + game.play('O', 0, 0); + }); + } + + @Test + void NotAllowPlayerToPlayInAnyPlayedPosition() { + assertThrows(IllegalArgumentException.class, () -> { + game.play('X', 0, 0); + game.play('O', 1, 0); + game.play('X', 0, 0); + }); + } + + @Test + void DeclareNoWinnerWithUnfinishedGrid() { + game.play('X', 0, 0); + game.play('O', 1, 0); + game.play('X', 0, 1); + game.play('O', 1, 1); + + Optional winner = game.computeWinner(); + + assertTrue(winner.isEmpty()); + } + + @Test + void DeclareNoWinnerWithTiedGrid() { + game.play('X', 1, 1); + game.play('O', 0, 0); + game.play('X', 0, 2); + game.play('O', 2, 0); + game.play('X', 1, 0); + game.play('O', 1, 2); + game.play('X', 2, 1); + game.play('O', 0, 1); + game.play('X', 2, 2); + + Optional winner = game.computeWinner(); + + assertTrue(winner.isEmpty()); + } + + @Test + void DeclarePlayerXAsAWinnerIfThreeInTopRow() { + game.play('X', 0, 0); + game.play('O', 1, 0); + game.play('X', 0, 1); + game.play('O', 1, 1); + game.play('X', 0, 2); + + Optional winner = game.computeWinner(); + + assertTrue(winner.isPresent()); + assertEquals(Symbol.X, winner.get()); + } + + @Test + void DeclarePlayerOAsAWinnerIfThreeInTopRow() { + game.play('X', 2, 2); + game.play('O', 0, 0); + game.play('X', 1, 0); + game.play('O', 0, 1); + game.play('X', 1, 1); + game.play('O', 0, 2); + + Optional winner = game.computeWinner(); + + assertTrue(winner.isPresent()); + assertEquals(Symbol.O, winner.get()); + } + + @Test + void DeclarePlayerXAsAWinnerIfThreeInMiddleRow() { + game.play('X', 1, 0); + game.play('O', 0, 0); + game.play('X', 1, 1); + game.play('O', 0, 1); + game.play('X', 1, 2); + + Optional winner = game.computeWinner(); + + assertTrue(winner.isPresent()); + assertEquals(Symbol.X, winner.get()); + } + + @Test + void DeclarePlayerOAsAWinnerIfThreeInMiddleRow() { + game.play('X', 0, 0); + game.play('O', 1, 0); + game.play('X', 2, 0); + game.play('O', 1, 1); + game.play('X', 2, 1); + game.play('O', 1, 2); + + Optional winner = game.computeWinner(); + + assertTrue(winner.isPresent()); + assertEquals(Symbol.O, winner.get()); + } + + @Test + void DeclarePlayerXAsAWinnerIfThreeInBottomRow() { + game.play('X', 2, 0); + game.play('O', 0, 0); + game.play('X', 2, 1); + game.play('O', 0, 1); + game.play('X', 2, 2); + + Optional winner = game.computeWinner(); + + assertTrue(winner.isPresent()); + assertEquals(Symbol.X, winner.get()); + } + + @Test + void DeclarePlayerOAsAWinnerIfThreeInBottomRow() { + game.play('X', 0, 0); + game.play('O', 2, 0); + game.play('X', 1, 0); + game.play('O', 2, 1); + game.play('X', 1, 1); + game.play('O', 2, 2); + + Optional winner = game.computeWinner(); + + assertTrue(winner.isPresent()); + assertEquals(Symbol.O, winner.get()); + } + + @Test + void NotAllowPlayingIllegalSymbol() { + game.play('X', 0, 0); + game.play('O', 2, 0); + assertThrows(IllegalArgumentException.class, () -> game.play('Y', 1, 0)); + } + + @Test + void AllowPlayingLowercaseVariants() { + game.play('X', 0, 0); + game.play('O', 2, 0); + assertDoesNotThrow(() -> game.play('x', 1, 0)); + assertDoesNotThrow(() -> game.play('o', 2, 1)); + } + + @Test + void DeclarePlayerXAsAWinnerIfThreeInLeftColumn() { + game.play('X', 0, 0); + game.play('O', 1, 0); + game.play('X', 0, 1); + game.play('O', 1, 1); + game.play('X', 0, 2); + + assertTrue(game.computeWinner().isPresent()); + assertEquals(Symbol.X, game.computeWinner().get()); + } + + @Test + @Disabled("Not implemented") + void DeclarePlayerXAsAWinnerIfThreeDiagonallyRightDown() { + game.play('X', 0, 0); + game.play('O', 1, 0); + game.play('X', 1, 1); + game.play('O', 2, 1); + game.play('X', 2, 2); + + System.out.println(game.printBoard()); + assertTrue(game.computeWinner().isPresent()); + assertEquals(Symbol.X, game.computeWinner().get()); + } +} diff --git a/java/test/Game_Should.java b/java/test/Game_Should.java deleted file mode 100644 index 22afb59..0000000 --- a/java/test/Game_Should.java +++ /dev/null @@ -1,131 +0,0 @@ -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class Game_Should { - private Game game; - - @BeforeEach - public void InitializeGame(){ - game = new Game(); - } - - @Test - public void NotAllowPlayerOToPlayFirst() { - assertThrows(Exception.class, () -> game.Play('O', 0, 0)); - } - - @Test - public void NotAllowPlayerXToPlayTwiceInARow() { - assertThrows(Exception.class, () ->{ - game.Play('X', 0, 0); - game.Play('X', 1, 0); - }); - } - - @Test - public void NotAllowPlayerToPlayInLastPlayedPosition() { - assertThrows(Exception.class, () ->{ - game.Play('X', 0, 0); - game.Play('O', 0, 0); - }); - } - - @Test - public void NotAllowPlayerToPlayInAnyPlayedPosition() { - assertThrows(Exception.class, () ->{ - game.Play('X', 0, 0); - game.Play('O', 1, 0); - game.Play('X', 0, 0); - }); - } - - @Test - public void DeclarePlayerXAsAWinnerIfThreeInTopRow() throws Exception - { - game.Play('X', 0, 0); - game.Play('O', 1, 0); - game.Play('X', 0, 1); - game.Play('O', 1, 1); - game.Play('X', 0, 2); - - char winner = game.Winner(); - - assertEquals('X', winner); - } - - @Test - public void DeclarePlayerOAsAWinnerIfThreeInTopRow() throws Exception - { - game.Play('X', 2, 2); - game.Play('O', 0, 0); - game.Play('X', 1, 0); - game.Play('O', 0, 1); - game.Play('X', 1, 1); - game.Play('O', 0, 2); - - char winner = game.Winner(); - - assertEquals('O', winner); - } - - @Test - public void DeclarePlayerXAsAWinnerIfThreeInMiddleRow() throws Exception - { - game.Play('X', 1, 0); - game.Play('O', 0, 0); - game.Play('X', 1, 1); - game.Play('O', 0, 1); - game.Play('X', 1, 2); - - char winner = game.Winner(); - - assertEquals('X', winner); - } - - @Test - public void DeclarePlayerOAsAWinnerIfThreeInMiddleRow() throws Exception - { - game.Play('X', 0, 0); - game.Play('O', 1, 0); - game.Play('X', 2, 0); - game.Play('O', 1, 1); - game.Play('X', 2, 1); - game.Play('O', 1, 2); - - char winner = game.Winner(); - - assertEquals('O', winner); - } - - @Test - public void DeclarePlayerXAsAWinnerIfThreeInBottomRow() throws Exception - { - game.Play('X', 2, 0); - game.Play('O', 0, 0); - game.Play('X', 2, 1); - game.Play('O', 0, 1); - game.Play('X', 2, 2); - - char winner = game.Winner(); - - assertEquals('X', winner); - } - - @Test - public void DeclarePlayerOAsAWinnerIfThreeInBottomRow() throws Exception - { - game.Play('X', 0, 0); - game.Play('O', 2, 0); - game.Play('X', 1, 0); - game.Play('O', 2, 1); - game.Play('X', 1, 1); - game.Play('O', 2, 2); - - char winner = game.Winner(); - - assertEquals('O', winner); - } -}