This commit is contained in:
2026-02-07 12:53:08 +01:00
parent 1492f078f9
commit a425be712e
17 changed files with 480 additions and 98 deletions

20
src/Ex6/App.java Normal file
View File

@@ -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));
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -1,5 +0,0 @@
package Ex6;
public abstract class Packet {
public abstract void accept(PacketVisitor visitor);
}

View File

@@ -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);
}

69
src/Ex6/Serveur.java Normal file
View File

@@ -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();
}
}
}
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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{

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);
}
}