diff --git a/src/Ex6/App.java b/src/Ex6/App.java new file mode 100644 index 0000000..8ceccdc --- /dev/null +++ b/src/Ex6/App.java @@ -0,0 +1,20 @@ +package Ex6; + +import java.io.IOException; +import java.net.Socket; + +public class App { + + /** + * Run this to launch the server and immediatly connect to it with a CLI Client. + */ + public static void main(String[] args) throws IOException { + final int port = 6666; + + System.out.println("Launching server ..."); + Serveur serveur = new Serveur(port); + + System.out.println("Connecting client1 ..."); + Client client1 = new Client(new Socket("localhost", port)); + } +} diff --git a/src/Ex6/Client.java b/src/Ex6/Client.java index 193be04..f467010 100644 --- a/src/Ex6/Client.java +++ b/src/Ex6/Client.java @@ -2,24 +2,127 @@ package Ex6; import java.io.IOException; import java.net.Socket; +import java.util.Arrays; +import java.util.Scanner; -import Ex6.packets.EndGamePacket; -import Ex6.packets.InvalidMovePacket; -import Ex6.packets.LeavePacket; -import Ex6.packets.NewGamePacket; -import Ex6.packets.PlayMovePacket; -import Ex6.packets.PlayerMovePacket; +import Ex6.Jeu.EtatCase; +import Ex6.network.Connexion; +import Ex6.network.packets.EndGamePacket; +import Ex6.network.packets.InvalidMovePacket; +import Ex6.network.packets.LeavePacket; +import Ex6.network.packets.NewGamePacket; +import Ex6.network.packets.PlayMovePacket; +import Ex6.network.packets.PlayerMovePacket; public class Client extends Connexion { + private EtatCase player = EtatCase.Vide; + private EtatCase turn = EtatCase.Vide; + private EtatCase[] cases = new EtatCase[9]; + + // I swear I typed those by hand + private static final String[] CELL_EMOJIS = { + "0️⃣ ", "1️⃣ ", "2️⃣ ", + "3️⃣ ", "4️⃣ ", "5️⃣ ", + "6️⃣ ", "7️⃣ ", "8️⃣ ", + }; + + /** + * Run this to connect to an existing server + */ + public static void main(String[] args) throws IOException { + new Client(new Socket("localhost", 6666)); + } + public Client(Socket socket) throws IOException { super(socket); + Scanner scanner = new Scanner(System.in); + boolean done = false; + while (!done) { + String line = scanner.nextLine(); + switch (line) { + case "CLOSE": + case "close": + case "STOP": + case "stop": + sendPacket(new LeavePacket()); + socket.close(); + done = true; + break; + + default: + break; + } + + try { + int cellIndex = Integer.parseInt(line); + if (cellIndex > 8 || cellIndex < 0) { + System.out.println("Number not in range !"); + continue; + } + sendPacket(new PlayMovePacket(cellIndex)); + } catch (NumberFormatException e) { + System.out.println("Number not recognized !"); + } + } + scanner.close(); + System.out.println("Bye !"); + } + + private boolean isPlayerTurn() { + return this.player == this.turn; + } + + private String getPlayerCharacter() { + return this.player == EtatCase.Rond ? "⭕" : "❌"; + } + + private void switchTurn() { + if (this.turn == EtatCase.Croix) { + this.turn = EtatCase.Rond; + } else { + this.turn = EtatCase.Croix; + } + } + + private String renderBoard() { + String board = ""; + for (int i = 0; i < 9; i++) { + EtatCase cell = cases[i]; + switch (cell) { + case Vide: + board += CELL_EMOJIS[i]; + break; + + case Croix: + board += "❌"; + break; + + case Rond: + board += "⭕"; + break; + } + if (i % 3 == 2) + board += "\n"; + } + return board; + } + + private void printBoard() { + System.out.println("\n\n\n\n\n\n\n\n\n"); + System.out.println(renderBoard()); + if (isPlayerTurn()) { + System.out.println("C'est à votre tour !"); + } else { + System.out.println("En attente de l'adversaire ..."); + } } @Override public void visit(EndGamePacket packet) { System.out.println("Jeu terminé !"); - // TODO: afficher vainqueur + printBoard(); + System.out.println("\n\n\nVous avez " + (packet.getVainqueur() == player ? "gagné 🤩" : "perdu 💩")); } @Override @@ -29,13 +132,19 @@ public class Client extends Connexion { @Override public void visit(NewGamePacket packet) { - System.out.println("Nouvelle partie !"); - // TODO: afficher tour + this.player = packet.isRond() ? EtatCase.Rond : EtatCase.Croix; + this.turn = EtatCase.Croix; + Arrays.fill(cases, EtatCase.Vide); + System.out.printf("Nouvelle partie !\n", this.getPlayerCharacter()); + System.out.println("Vous êtes les " + getPlayerCharacter()); + printBoard(); } @Override public void visit(PlayerMovePacket packet) { - // TODO: afficher plateau + cases[packet.getCellIndex()] = turn; + switchTurn(); + printBoard(); } @Override diff --git a/src/Ex6/Jeu.java b/src/Ex6/Jeu.java index 401536a..78f6bef 100644 --- a/src/Ex6/Jeu.java +++ b/src/Ex6/Jeu.java @@ -1,5 +1,8 @@ package Ex6; +import java.util.Arrays; +import java.util.Random; + public class Jeu { public enum EtatCase { Croix, @@ -7,9 +10,106 @@ public class Jeu { Vide } - boolean tourDeRond = false; + private boolean gameDone; - boolean gameDone = false; + private EtatCase turn = EtatCase.Vide; + private EtatCase[] cases = new EtatCase[9]; - EtatCase cases[3][3]; + private final Serveur serveur; + + private static final int[][] CHECK_INDICES = { + // rows + {0, 1, 2}, {3, 4, 5}, {6, 7, 8}, + + // columns + {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, + + // diagonals + {0, 4, 8}, {2, 4, 6} + }; + + public Jeu(Serveur serveur) { + this.serveur = serveur; + } + + /** + * Start a new game and randomly assign symbols to players + */ + public void start() { + boolean player1Rond = new Random().nextBoolean(); + + this.serveur.joueur1.startGame(player1Rond); + this.serveur.joueur2.startGame(!player1Rond); + + this.gameDone = false; + this.turn = EtatCase.Croix; + Arrays.fill(cases, EtatCase.Vide); + } + + private void switchTurn() { + if (this.turn == EtatCase.Croix){ + this.turn = EtatCase.Rond; + } else { + this.turn = EtatCase.Croix; + } + } + + /** + * Check for winning pattern by looking at an index table. + * @return EtatCase.Vide if no one won. + */ + private EtatCase checkWin() { + for (int i = 0; i < CHECK_INDICES.length; i++) { + EtatCase previousCell = null; + boolean skip = false; + for (int j = 0; j < 3; j++) { + int cellIndex = CHECK_INDICES[i][j]; + EtatCase cell = cases[cellIndex]; + if (cell == EtatCase.Vide) { + skip = true; + break; + } + if (previousCell != null) { + if (previousCell != cell) { + skip = true; + break; + } + } + previousCell = cell; + } + if (skip) + continue; + + return previousCell; + } + return EtatCase.Vide; + } + + /** + * Apply a move for the current player. + * Be aware that the move is not checked and should be done before. + * @see Ex6.Jeu.checkWin + * @param cellIndex index of the cell to replace. + * @return EtatCase.Vide if no one won. + */ + public EtatCase playMove(int cellIndex) { + cases[cellIndex] = this.turn; + switchTurn(); + EtatCase winner = checkWin(); + if (winner != EtatCase.Vide) + this.gameDone = true; + return winner; + } + + public EtatCase getCell(int index) { + return cases[index]; + } + + public EtatCase getTurn() { + return turn; + } + + public boolean isGameDone() { + return this.gameDone; + } } diff --git a/src/Ex6/Joueur.java b/src/Ex6/Joueur.java index 15f42fa..efba9e5 100644 --- a/src/Ex6/Joueur.java +++ b/src/Ex6/Joueur.java @@ -3,28 +3,34 @@ package Ex6; import java.io.IOException; import java.net.Socket; -import Ex6.packets.EndGamePacket; -import Ex6.packets.InvalidMovePacket; -import Ex6.packets.LeavePacket; -import Ex6.packets.NewGamePacket; -import Ex6.packets.PlayMovePacket; -import Ex6.packets.PlayerMovePacket; +import Ex6.Jeu.EtatCase; +import Ex6.network.Connexion; +import Ex6.network.packets.EndGamePacket; +import Ex6.network.packets.InvalidMovePacket; +import Ex6.network.packets.LeavePacket; +import Ex6.network.packets.NewGamePacket; +import Ex6.network.packets.PlayMovePacket; +import Ex6.network.packets.PlayerMovePacket; public class Joueur extends Connexion { - public Joueur(Socket socket) throws IOException { + private final Jeu jeu; + private final Serveur serveur; + private EtatCase player; + + public Joueur(Socket socket, Jeu jeu, Serveur serveur) throws IOException { super(socket); - new Thread(this::readPackets).start(); + this.jeu = jeu; + this.serveur = serveur; } - private void readPackets() { - while (true) { - try { - processPacket(); - } catch (ClassNotFoundException | IOException e) { - e.printStackTrace(); - } - } + /** + * Notify a new game has started to the client + * @param rond True if player is 'O' + */ + public void startGame(boolean rond) { + this.player = rond ? EtatCase.Rond : EtatCase.Croix; + sendPacket(new NewGamePacket(rond)); } @Override @@ -34,7 +40,31 @@ public class Joueur extends Connexion { @Override public void visit(PlayMovePacket packet) { - //TODO: vérifier coups + // player should not play + if (this.jeu.isGameDone() || this.jeu.getTurn() != this.player) { + sendPacket(new InvalidMovePacket()); + return; + } + + // check placing on empty cell + if (this.jeu.getCell(packet.getCellIndex()) != EtatCase.Vide) { + sendPacket(new InvalidMovePacket()); + return; + } + + EtatCase winner = this.jeu.playMove(packet.getCellIndex()); + this.serveur.broadcastPacket(new PlayerMovePacket(packet.getCellIndex())); + if (winner != EtatCase.Vide) { + this.serveur.broadcastPacket(new EndGamePacket(winner)); + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // automatically launch new game after 10s + this.jeu.start(); + } } @Override diff --git a/src/Ex6/Packet.java b/src/Ex6/Packet.java deleted file mode 100644 index 16d54ac..0000000 --- a/src/Ex6/Packet.java +++ /dev/null @@ -1,5 +0,0 @@ -package Ex6; - -public abstract class Packet { - public abstract void accept(PacketVisitor visitor); -} diff --git a/src/Ex6/PacketVisitor.java b/src/Ex6/PacketVisitor.java deleted file mode 100644 index dea7cb9..0000000 --- a/src/Ex6/PacketVisitor.java +++ /dev/null @@ -1,22 +0,0 @@ -package Ex6; - -import Ex6.packets.EndGamePacket; -import Ex6.packets.InvalidMovePacket; -import Ex6.packets.LeavePacket; -import Ex6.packets.NewGamePacket; -import Ex6.packets.PlayMovePacket; -import Ex6.packets.PlayerMovePacket; - -public interface PacketVisitor { - void visit(EndGamePacket packet); - - void visit(InvalidMovePacket packet); - - void visit(LeavePacket packet); - - void visit(NewGamePacket packet); - - void visit(PlayerMovePacket packet); - - void visit(PlayMovePacket packet); -} diff --git a/src/Ex6/Serveur.java b/src/Ex6/Serveur.java new file mode 100644 index 0000000..bce54fa --- /dev/null +++ b/src/Ex6/Serveur.java @@ -0,0 +1,69 @@ +package Ex6; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +import Ex6.network.Packet; + +public class Serveur { + + private final Jeu jeu; + + private final ServerSocket serverSocket; + public Joueur joueur1; + public Joueur joueur2; + + /** + * @brief Run this to launch the server in headless mode + */ + public static void main(String[] args) throws IOException { + new Serveur(6666); + } + + /** + * Start a TicTacTo server on the specified port + * @param port the port to listen to + * @throws IOException + */ + public Serveur(int port) throws IOException { + this.jeu = new Jeu(this); + this.serverSocket = new ServerSocket(port); + this.joueur1 = null; + this.joueur2 = null; + new Thread(this::acceptLoop).start(); + } + + /** + * Send a packet to both players + * @param packet the packet to send + */ + public void broadcastPacket(Packet packet) { + this.joueur1.sendPacket(packet); + this.joueur2.sendPacket(packet); + } + + /** + * Here we accept only the first two players. + * If a player leaves, the server needs to be restarted. + * It's not robust but enough for a demo. + */ + private void acceptLoop() { + while (true) { + try { + Socket socket = this.serverSocket.accept(); + if (this.joueur1 == null) { + this.joueur1 = new Joueur(socket, jeu, this); + continue; + } + if (this.joueur2 == null) { + this.joueur2 = new Joueur(socket, jeu, this); + this.jeu.start(); + break; + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/Ex6/Connexion.java b/src/Ex6/network/Connexion.java similarity index 54% rename from src/Ex6/Connexion.java rename to src/Ex6/network/Connexion.java index 238ddd9..f16d530 100644 --- a/src/Ex6/Connexion.java +++ b/src/Ex6/network/Connexion.java @@ -1,22 +1,29 @@ -package Ex6; +package Ex6.network; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; +/** + * Abstraction of TCP connection implementing PacketVisitor + */ public abstract class Connexion implements PacketVisitor { private final Socket socket; private final ObjectOutputStream out; - private final ObjectInputStream in; + private ObjectInputStream in; public Connexion(Socket socket) throws IOException { this.socket = socket; this.out = new ObjectOutputStream(this.socket.getOutputStream()); - this.in = new ObjectInputStream(this.socket.getInputStream()); + new Thread(this::readPackets).start(); } + /** + * Send a packet through network + * @param packet The packet to send + */ public void sendPacket(Packet packet) { try { out.writeObject(packet); @@ -25,17 +32,45 @@ public abstract class Connexion implements PacketVisitor { } } + /** + * Loop to process new packets infinitely. + */ + private void readPackets() { + try { + this.in = new ObjectInputStream(this.socket.getInputStream()); + while (true) { + try { + processPacket(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Read a packet. + * @return The packet read. + */ private Packet readPacket() throws ClassNotFoundException, IOException { Object o = in.readObject(); Packet packet = (Packet) o; return packet; } + /** + * Processing the packet through double-dispatch. + */ public void processPacket() throws ClassNotFoundException, IOException { Packet packet = readPacket(); - packet.accept(this); + visit(packet); } + /** + * Close the connection. + */ protected void close() { try { this.socket.close(); diff --git a/src/Ex6/network/Packet.java b/src/Ex6/network/Packet.java new file mode 100644 index 0000000..8a086d2 --- /dev/null +++ b/src/Ex6/network/Packet.java @@ -0,0 +1,8 @@ +package Ex6.network; + +/** + * Abstract class representing a packet (implementing the Visitor design pattern) + */ +public abstract class Packet { + public abstract void accept(PacketVisitor visitor); +} diff --git a/src/Ex6/network/PacketVisitor.java b/src/Ex6/network/PacketVisitor.java new file mode 100644 index 0000000..7daa693 --- /dev/null +++ b/src/Ex6/network/PacketVisitor.java @@ -0,0 +1,29 @@ +package Ex6.network; + +import Ex6.network.packets.EndGamePacket; +import Ex6.network.packets.InvalidMovePacket; +import Ex6.network.packets.LeavePacket; +import Ex6.network.packets.NewGamePacket; +import Ex6.network.packets.PlayMovePacket; +import Ex6.network.packets.PlayerMovePacket; + +public interface PacketVisitor { + void visit(EndGamePacket packet); + + void visit(InvalidMovePacket packet); + + void visit(LeavePacket packet); + + void visit(NewGamePacket packet); + + void visit(PlayerMovePacket packet); + + void visit(PlayMovePacket packet); + + /** + * Double-dispatch + */ + default void visit(Packet packet) { + packet.accept(this); + } +} diff --git a/src/Ex6/packets/EndGamePacket.java b/src/Ex6/network/packets/EndGamePacket.java similarity index 71% rename from src/Ex6/packets/EndGamePacket.java rename to src/Ex6/network/packets/EndGamePacket.java index d69bacf..334869c 100644 --- a/src/Ex6/packets/EndGamePacket.java +++ b/src/Ex6/network/packets/EndGamePacket.java @@ -1,13 +1,13 @@ -package Ex6.packets; +package Ex6.network.packets; import java.io.Serializable; -import Ex6.Packet; -import Ex6.PacketVisitor; import Ex6.Jeu.EtatCase; +import Ex6.network.Packet; +import Ex6.network.PacketVisitor; /** - * @brief Packet envoyé pour annoncer la fin de la partie + * Packet sent to both clients to end a game (providing the winner) */ public class EndGamePacket extends Packet implements Serializable{ diff --git a/src/Ex6/packets/InvalidMovePacket.java b/src/Ex6/network/packets/InvalidMovePacket.java similarity index 54% rename from src/Ex6/packets/InvalidMovePacket.java rename to src/Ex6/network/packets/InvalidMovePacket.java index b6b3f2e..731d85d 100644 --- a/src/Ex6/packets/InvalidMovePacket.java +++ b/src/Ex6/network/packets/InvalidMovePacket.java @@ -1,16 +1,18 @@ -package Ex6.packets; +package Ex6.network.packets; import java.io.Serializable; -import Ex6.Packet; -import Ex6.PacketVisitor; +import Ex6.network.Packet; +import Ex6.network.PacketVisitor; +/** + * Packet send to a client to indicate an invalid move. + */ public class InvalidMovePacket extends Packet implements Serializable { - public InvalidMovePacket() { - } @Override public void accept(PacketVisitor visitor) { visitor.visit(this); } + } diff --git a/src/Ex6/network/packets/LeavePacket.java b/src/Ex6/network/packets/LeavePacket.java new file mode 100644 index 0000000..32b62af --- /dev/null +++ b/src/Ex6/network/packets/LeavePacket.java @@ -0,0 +1,18 @@ +package Ex6.network.packets; + +import java.io.Serializable; + +import Ex6.network.Packet; +import Ex6.network.PacketVisitor; + +/** + * Packet sent by a client to indicate he is leaving. + */ +public class LeavePacket extends Packet implements Serializable { + + @Override + public void accept(PacketVisitor visitor) { + visitor.visit(this); + } + +} diff --git a/src/Ex6/packets/NewGamePacket.java b/src/Ex6/network/packets/NewGamePacket.java similarity index 59% rename from src/Ex6/packets/NewGamePacket.java rename to src/Ex6/network/packets/NewGamePacket.java index baa8e0d..e76c1c2 100644 --- a/src/Ex6/packets/NewGamePacket.java +++ b/src/Ex6/network/packets/NewGamePacket.java @@ -1,11 +1,14 @@ -package Ex6.packets; +package Ex6.network.packets; import java.io.Serializable; -import Ex6.Packet; -import Ex6.PacketVisitor; +import Ex6.network.Packet; +import Ex6.network.PacketVisitor; -public class NewGamePacket extends Packet implements Serializable{ +/** + * Packet sent to both client to indicate a new game has started (providing the symbol of the player for this game). + */ +public class NewGamePacket extends Packet implements Serializable { private final boolean rond; diff --git a/src/Ex6/packets/PlayMovePacket.java b/src/Ex6/network/packets/PlayMovePacket.java similarity index 69% rename from src/Ex6/packets/PlayMovePacket.java rename to src/Ex6/network/packets/PlayMovePacket.java index 77fba0c..3aa095d 100644 --- a/src/Ex6/packets/PlayMovePacket.java +++ b/src/Ex6/network/packets/PlayMovePacket.java @@ -1,10 +1,13 @@ -package Ex6.packets; +package Ex6.network.packets; import java.io.Serializable; -import Ex6.Packet; -import Ex6.PacketVisitor; +import Ex6.network.Packet; +import Ex6.network.PacketVisitor; +/** + * Packet sent by a client to draw a symbol in a cell. + */ public class PlayMovePacket extends Packet implements Serializable{ private final int cellIndex; diff --git a/src/Ex6/packets/PlayerMovePacket.java b/src/Ex6/network/packets/PlayerMovePacket.java similarity index 69% rename from src/Ex6/packets/PlayerMovePacket.java rename to src/Ex6/network/packets/PlayerMovePacket.java index c5814f1..93f5fbe 100644 --- a/src/Ex6/packets/PlayerMovePacket.java +++ b/src/Ex6/network/packets/PlayerMovePacket.java @@ -1,13 +1,12 @@ -package Ex6.packets; +package Ex6.network.packets; import java.io.Serializable; -import Ex6.Packet; -import Ex6.PacketVisitor; +import Ex6.network.Packet; +import Ex6.network.PacketVisitor; /** - * @brief Packet envoyé par le serveur pour confirmer un coup et changer le - * tour. + * Packet sent to both client to confirm a move and switch turns. */ public class PlayerMovePacket extends Packet implements Serializable { diff --git a/src/Ex6/packets/LeavePacket.java b/src/Ex6/packets/LeavePacket.java deleted file mode 100644 index 3bbee74..0000000 --- a/src/Ex6/packets/LeavePacket.java +++ /dev/null @@ -1,16 +0,0 @@ -package Ex6.packets; - -import java.io.Serializable; - -import Ex6.Packet; -import Ex6.PacketVisitor; - -public class LeavePacket extends Packet implements Serializable{ - public LeavePacket() { - } - - @Override - public void accept(PacketVisitor visitor) { - visitor.visit(this); - } -}