feat: Intégration WebSocket pour l'interaction multijoueur
Remplace le polling REST inefficace et les tentatives d'envoi de mouvements via REST par une communication WebSocket complète dans MultiplayerActivity. Modifications clés : - Ajout de la dépendance OkHttp pour le support WebSocket (action manuelle). - Utilisation de OkHttp pour établir et gérer la connexion WebSocket (, ). - Implémentation d'un () pour gérer les événements : - Envoi du message 'register' à l'ouverture (). - Réception et traitement des messages 'gameStateUpdate', 'error', 'info' (). - Gestion de la fermeture et des erreurs (, , ). - Mise à jour de l'interface utilisateur (, ) en temps réel basée sur les messages reçus (en utilisant ). - Modification de pour créer et envoyer les messages 'move' via WebSocket (). - Suppression complète du mécanisme de polling basé sur . - Gestion du cycle de vie de la connexion WebSocket dans , , . - Ajout de classes de données internes pour parser/créer les messages WebSocket JSON (, , , etc.).
This commit is contained in:
parent
1dc439c186
commit
be983a1107
@ -45,4 +45,5 @@ dependencies {
|
||||
implementation(libs.converter.gson)
|
||||
implementation(libs.logging.interceptor)
|
||||
implementation(libs.gson)
|
||||
implementation(libs.okhttp)
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package legion.muyue.best2048;
|
||||
|
||||
// Imports Android standard et UI
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
@ -11,29 +12,45 @@ import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.gridlayout.widget.GridLayout;
|
||||
|
||||
import java.util.Objects; // Pour la comparaison d'ID
|
||||
|
||||
import legion.muyue.best2048.data.GameInfo;
|
||||
import legion.muyue.best2048.data.GameStateResponse;
|
||||
import legion.muyue.best2048.data.MoveRequest;
|
||||
import legion.muyue.best2048.data.PlayerIdRequest;
|
||||
import legion.muyue.best2048.network.ApiClient;
|
||||
import legion.muyue.best2048.network.ApiService;
|
||||
// Imports Retrofit (pour les appels REST initiaux)
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.Response; // <--- IMPORT IMPORTANT POUR RETROFIT CALLBACK !
|
||||
|
||||
// Imports OkHttp (pour WebSocket)
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
// import okhttp3.Response; // Aussi nécessaire pour WebSocketListener.onFailure, garder les deux imports
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
import okio.ByteString;
|
||||
|
||||
// Imports Gson (pour JSON)
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
// Import Timeout pour OkHttpClient
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
// Imports des classes Data et Network (existantes)
|
||||
import legion.muyue.best2048.data.GameInfo;
|
||||
import legion.muyue.best2048.data.GameStateResponse;
|
||||
import legion.muyue.best2048.data.MoveRequest; // Utilisé pour construire le JSON du move via WebSocket
|
||||
import legion.muyue.best2048.data.PlayerIdRequest; // Pour createOrJoinGame
|
||||
import legion.muyue.best2048.network.ApiClient;
|
||||
import legion.muyue.best2048.network.ApiService;
|
||||
|
||||
|
||||
public class MultiplayerActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "MultiplayerActivity";
|
||||
private static final int BOARD_SIZE = 4;
|
||||
private static final long POLLING_INTERVAL_MS = 3000; // Intervalle de polling (3 sec) - Inefficace !
|
||||
|
||||
// UI
|
||||
private GridLayout boardGridLayoutMulti;
|
||||
@ -44,47 +61,81 @@ public class MultiplayerActivity extends AppCompatActivity {
|
||||
private ProgressBar loadingIndicatorMulti;
|
||||
private TextView[][] tileViewsMulti = new TextView[BOARD_SIZE][BOARD_SIZE];
|
||||
|
||||
// Network
|
||||
// Network - REST (pour appels initiaux)
|
||||
private ApiService apiService;
|
||||
|
||||
// Network - WebSocket (pour temps réel)
|
||||
private OkHttpClient wsClient;
|
||||
private WebSocket webSocket;
|
||||
private MyWebSocketListener webSocketListener;
|
||||
private static final String WEBSOCKET_URL = "wss://best2048.legion-muyue.fr/ws";
|
||||
private boolean isWebSocketConnected = false;
|
||||
|
||||
// Game State
|
||||
private String currentGameId = null;
|
||||
private String myPlayerId = "Player1_Temp"; // TODO: Remplacer par une vraie identification
|
||||
private String myPlayerId = null;
|
||||
private String opponentPlayerId = null;
|
||||
private GameStateResponse currentGameState = null;
|
||||
|
||||
// Polling Handler
|
||||
private Handler pollingHandler;
|
||||
private Runnable pollingRunnable;
|
||||
private boolean isPollingActive = false;
|
||||
// JSON Parser
|
||||
private Gson gson;
|
||||
|
||||
// Handler pour UI Thread
|
||||
private Handler uiHandler;
|
||||
|
||||
// --- Activity Lifecycle ---
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_multiplayer);
|
||||
Log.d(TAG, "onCreate");
|
||||
|
||||
findViewsMulti();
|
||||
apiService = ApiClient.getApiService();
|
||||
pollingHandler = new Handler(Looper.getMainLooper());
|
||||
apiService = ApiClient.getApiService(); // Instance Retrofit
|
||||
wsClient = new OkHttpClient.Builder()
|
||||
.pingInterval(30, TimeUnit.SECONDS) // Maintenir connexion WS active
|
||||
.build();
|
||||
webSocketListener = new MyWebSocketListener();
|
||||
gson = new Gson();
|
||||
uiHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
setupSwipeListenerMulti();
|
||||
initMultiplayerGame();
|
||||
initMultiplayerGame(); // Démarrer le processus
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
stopPolling(); // Arrête le polling quand l'activité n'est plus visible
|
||||
Log.d(TAG, "onPause");
|
||||
closeWebSocket(); // Fermer WS en quittant l'écran
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (currentGameId != null && currentGameState != null && !currentGameState.isGameOver()) {
|
||||
startPolling(); // Redémarre le polling si une partie est en cours
|
||||
Log.d(TAG, "onResume");
|
||||
// Tenter de reconnecter si besoin
|
||||
if (currentGameId != null && !isWebSocketConnected && currentGameState != null && !currentGameState.isGameOver()) {
|
||||
Log.i(TAG, "Tentative de reconnexion WebSocket...");
|
||||
connectWebSocket();
|
||||
} else if (currentGameId != null && isWebSocketConnected) {
|
||||
Log.d(TAG, "WebSocket déjà connecté, rafraîchissement état via REST...");
|
||||
fetchGameState(); // Resynchroniser via REST au cas où
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.d(TAG, "onDestroy");
|
||||
closeWebSocket(); // Assurer la fermeture
|
||||
if (uiHandler != null) {
|
||||
uiHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI Setup ---
|
||||
private void findViewsMulti() {
|
||||
boardGridLayoutMulti = findViewById(R.id.gameBoardMulti);
|
||||
myScoreLabelMulti = findViewById(R.id.myScoreLabelMulti);
|
||||
@ -94,160 +145,293 @@ public class MultiplayerActivity extends AppCompatActivity {
|
||||
loadingIndicatorMulti = findViewById(R.id.loadingIndicatorMulti);
|
||||
}
|
||||
|
||||
/** Tente de créer ou rejoindre une partie multijoueur. */
|
||||
// --- Game Initialization & REST Call ---
|
||||
|
||||
/** Lance l'appel REST pour créer ou rejoindre une partie. */
|
||||
private void initMultiplayerGame() {
|
||||
showLoading(true);
|
||||
statusTextMulti.setText("Recherche d'une partie...");
|
||||
statusTextMulti.setText(R.string.multiplayer_status_searching);
|
||||
|
||||
// !!! IMPORTANT: Générer un ID unique ici !!!
|
||||
// myPlayerId = "Player1_Temp"; // À REMPLACER
|
||||
myPlayerId = java.util.UUID.randomUUID().toString(); // Exemple simple d'ID unique
|
||||
Log.i(TAG, "Utilisation du Player ID: " + myPlayerId);
|
||||
// Générer/Récupérer Player ID (UUID temporaire pour test)
|
||||
myPlayerId = java.util.UUID.randomUUID().toString();
|
||||
Log.i(TAG, "initMultiplayerGame: Utilisation du Player ID: " + myPlayerId);
|
||||
|
||||
// Crée l'objet pour le corps de la requête
|
||||
PlayerIdRequest requestBody = new PlayerIdRequest(myPlayerId);
|
||||
|
||||
// Utilise requestBody dans l'appel API
|
||||
// Appel Retrofit
|
||||
apiService.createOrJoinGame(requestBody).enqueue(new Callback<GameInfo>() {
|
||||
@Override
|
||||
// Utilise bien retrofit2.Response ici
|
||||
public void onResponse(@NonNull Call<GameInfo> call, @NonNull Response<GameInfo> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
GameInfo gameInfo = response.body();
|
||||
currentGameId = gameInfo.getGameId();
|
||||
|
||||
// Détermine correctement l'ID de l'adversaire
|
||||
// Détermine opponentPlayerId
|
||||
if (myPlayerId.equals(gameInfo.getPlayer1Id())) {
|
||||
opponentPlayerId = gameInfo.getPlayer2Id();
|
||||
} else {
|
||||
opponentPlayerId = gameInfo.getPlayer1Id(); // Si je suis P2, l'autre est P1
|
||||
opponentPlayerId = gameInfo.getPlayer1Id();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Partie rejointe/créée: ID=" + currentGameId + ", Moi=" + myPlayerId + ", Adversaire=" + opponentPlayerId);
|
||||
statusTextMulti.setText("Partie trouvée ! ID: " + currentGameId.substring(0, 8) + "..."); // Affiche début ID
|
||||
fetchGameState(); // Récupère l'état initial
|
||||
Log.i(TAG, "Partie rejointe/créée via REST: ID=" + currentGameId + ", Moi=" + myPlayerId + ", Adversaire=" + opponentPlayerId);
|
||||
statusTextMulti.setText(getString(R.string.multiplayer_status_found, currentGameId.substring(0, 8)));
|
||||
|
||||
// Succès de l'appel REST -> On lance la connexion WebSocket
|
||||
connectWebSocket();
|
||||
|
||||
} else {
|
||||
Log.e(TAG, "Erreur création/rejoindre partie: " + response.code());
|
||||
handleNetworkError("Impossible de créer ou rejoindre une partie (Code: " + response.code() + ")");
|
||||
handleNetworkError(getString(R.string.error_join_create_game, response.code()));
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<GameInfo> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "Échec création/rejoindre partie", t);
|
||||
handleNetworkError("Échec de connexion au serveur.");
|
||||
handleNetworkError(getString(R.string.error_server_connection));
|
||||
showLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Récupère l'état actuel du jeu depuis le serveur. */
|
||||
/** Récupère l'état actuel via REST (pour synchro). */
|
||||
private void fetchGameState() {
|
||||
if (currentGameId == null) return;
|
||||
// Ne montre le chargement que si pas déjà en polling (pour fluidité)
|
||||
if(!isPollingActive) showLoading(true);
|
||||
Log.d(TAG, "fetchGameState via REST pour Game ID: " + currentGameId);
|
||||
|
||||
apiService.getGameState(currentGameId).enqueue(new Callback<GameStateResponse>() {
|
||||
@Override
|
||||
// Utilise bien retrofit2.Response ici
|
||||
public void onResponse(@NonNull Call<GameStateResponse> call, @NonNull Response<GameStateResponse> response) {
|
||||
showLoading(false);
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
Log.d(TAG, "État du jeu récupéré via REST.");
|
||||
currentGameState = response.body();
|
||||
Log.d(TAG, "État du jeu récupéré. Tour de: " + currentGameState.getCurrentPlayerId());
|
||||
updateMultiplayerUI(currentGameState); // Met à jour l'affichage
|
||||
if (!currentGameState.isGameOver()) {
|
||||
startPolling(); // Commence ou continue le polling
|
||||
} else {
|
||||
stopPolling(); // Arrête si la partie est finie
|
||||
// Afficher message fin de partie
|
||||
}
|
||||
// Mettre à jour l'UI sur le thread principal
|
||||
runOnUiThread(() -> updateMultiplayerUI(currentGameState));
|
||||
} else {
|
||||
Log.e(TAG, "Erreur récupération état partie: " + response.code());
|
||||
// Gérer l'erreur, peut-être réessayer ?
|
||||
statusTextMulti.setText("Erreur récupération état...");
|
||||
Log.e(TAG, "Erreur récupération état partie via REST: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<GameStateResponse> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "Échec récupération état partie", t);
|
||||
showLoading(false);
|
||||
handleNetworkError("Échec connexion pour état partie.");
|
||||
// Arrêter le polling en cas d'échec réseau ?
|
||||
// stopPolling();
|
||||
Log.e(TAG, "Échec récupération état partie via REST", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Démarre le polling périodique pour récupérer l'état du jeu. */
|
||||
private void startPolling() {
|
||||
if (isPollingActive || currentGameId == null || currentGameState == null || currentGameState.isGameOver()) {
|
||||
return; // Ne pas démarrer si déjà actif, pas de partie, ou partie finie
|
||||
}
|
||||
Log.d(TAG, "Démarrage du polling...");
|
||||
isPollingActive = true;
|
||||
pollingRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isPollingActive) return; // Vérifie si on doit s'arrêter
|
||||
fetchGameState(); // Récupère l'état
|
||||
// Replanifie après l'intervalle (même si fetchGameState est en cours)
|
||||
// Pour éviter accumulation, on pourrait replanifier dans onResponse/onFailure
|
||||
// Mais pour simple polling, c'est ok.
|
||||
pollingHandler.postDelayed(this, POLLING_INTERVAL_MS);
|
||||
}
|
||||
};
|
||||
pollingHandler.post(pollingRunnable); // Lance la première exécution
|
||||
}
|
||||
// --- WebSocket Management ---
|
||||
|
||||
|
||||
/** Arrête le polling périodique. */
|
||||
private void stopPolling() {
|
||||
if (isPollingActive && pollingHandler != null && pollingRunnable != null) {
|
||||
Log.d(TAG, "Arrêt du polling.");
|
||||
isPollingActive = false;
|
||||
pollingHandler.removeCallbacks(pollingRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
/** Met à jour l'interface multijoueur basée sur l'état reçu. */
|
||||
private void updateMultiplayerUI(GameStateResponse state) {
|
||||
if (state == null) return;
|
||||
|
||||
// Met à jour les scores
|
||||
// TODO: Adapter la logique getMyScore/getOpponentScore avec les vrais ID
|
||||
myScoreLabelMulti.setText("Moi:\n" + state.getMyScore(myPlayerId));
|
||||
opponentScoreLabelMulti.setText("Autre:\n" + state.getOpponentScore(myPlayerId));
|
||||
|
||||
// Met à jour l'indicateur de tour
|
||||
boolean myTurn = myPlayerId.equals(state.getCurrentPlayerId());
|
||||
turnIndicatorMulti.setText(myTurn ? "Votre Tour" : "Tour Adversaire");
|
||||
turnIndicatorMulti.setTextColor(ContextCompat.getColor(this, myTurn ? R.color.tile_16 : R.color.text_tile_low)); // Exemple couleur
|
||||
|
||||
// Met à jour le plateau
|
||||
syncBoardViewMulti(state.getBoard());
|
||||
|
||||
// Met à jour le statut
|
||||
if(state.isGameOver()){
|
||||
statusTextMulti.setText(state.getWinnerId() != null ? (state.getWinnerId().equals(myPlayerId) ? "Vous avez gagné !" : "Vous avez perdu.") : "Partie Terminée !");
|
||||
} else if (!myTurn) {
|
||||
statusTextMulti.setText("En attente du coup adverse...");
|
||||
} else {
|
||||
statusTextMulti.setText(""); // Vide si c'est notre tour et pas fini
|
||||
}
|
||||
}
|
||||
|
||||
/** Synchronise la vue de la grille multijoueur avec un état de plateau donné. */
|
||||
private void syncBoardViewMulti(int[][] boardState) {
|
||||
if (boardState == null || boardState.length != BOARD_SIZE || boardState[0].length != BOARD_SIZE) {
|
||||
Log.e(TAG, "syncBoardViewMulti: État du plateau invalide.");
|
||||
/** Initialise et lance la connexion WebSocket. */
|
||||
private void connectWebSocket() {
|
||||
if (currentGameId == null || myPlayerId == null) {
|
||||
Log.e(TAG, "connectWebSocket: Impossible de connecter, gameId ou playerId est null.");
|
||||
return;
|
||||
}
|
||||
// Logique similaire à syncBoardView de MainActivity
|
||||
boardGridLayoutMulti.removeAllViews(); // Simplification par reset complet
|
||||
tileViewsMulti = new TextView[BOARD_SIZE][BOARD_SIZE];
|
||||
if (webSocket != null) {
|
||||
Log.w(TAG, "connectWebSocket: Tentative de connexion alors qu'un WebSocket existe déjà. Fermeture de l'ancien.");
|
||||
closeWebSocket();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Tentative de connexion WebSocket à: " + WEBSOCKET_URL);
|
||||
statusTextMulti.setText(R.string.multiplayer_status_connecting);
|
||||
showLoading(true);
|
||||
|
||||
Request request = new Request.Builder().url(WEBSOCKET_URL).build();
|
||||
webSocket = wsClient.newWebSocket(request, webSocketListener); // Lance la connexion
|
||||
}
|
||||
|
||||
/** Ferme la connexion WebSocket proprement. */
|
||||
private void closeWebSocket() {
|
||||
if (webSocket != null) {
|
||||
Log.i(TAG, "Fermeture de la connexion WebSocket.");
|
||||
webSocket.close(1000, "Activity closed");
|
||||
webSocket = null;
|
||||
isWebSocketConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Méthode pour envoyer un message texte via le WebSocket. */
|
||||
private void sendWebSocketMessage(String jsonMessage) {
|
||||
if (webSocket != null && isWebSocketConnected) {
|
||||
Log.d(TAG, "Envoi WS message: " + jsonMessage);
|
||||
webSocket.send(jsonMessage);
|
||||
} else {
|
||||
Log.e(TAG, "sendWebSocketMessage: Tentative d'envoi alors que le WebSocket est fermé ou non connecté.");
|
||||
Toast.makeText(this, R.string.error_websocket_disconnected, Toast.LENGTH_SHORT).show();
|
||||
// On pourrait tenter une reconnexion ici si désiré
|
||||
// connectWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- WebSocket Listener Implementation ---
|
||||
private class MyWebSocketListener extends WebSocketListener {
|
||||
|
||||
@Override
|
||||
public void onOpen(@NonNull WebSocket ws, @NonNull okhttp3.Response response) { // okhttp3.Response ici !
|
||||
super.onOpen(ws, response);
|
||||
isWebSocketConnected = true;
|
||||
Log.i(TAG, "WebSocket onOpen: Connexion établie !");
|
||||
|
||||
// Envoie le message d'enregistrement
|
||||
RegisterMessage registerMsg = new RegisterMessage("register", myPlayerId, currentGameId);
|
||||
String registerJson = gson.toJson(registerMsg);
|
||||
ws.send(registerJson); // Envoie l'enregistrement
|
||||
|
||||
// Met à jour l'UI sur le thread principal
|
||||
runOnUiThread(() -> {
|
||||
showLoading(false);
|
||||
statusTextMulti.setText(R.string.multiplayer_status_waiting_state);
|
||||
Toast.makeText(MultiplayerActivity.this, R.string.websocket_connected, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(@NonNull WebSocket ws, @NonNull String text) {
|
||||
super.onMessage(ws, text);
|
||||
Log.d(TAG, "WebSocket onMessage: Reçu texte: " + text);
|
||||
|
||||
try {
|
||||
BaseMessage baseMessage = gson.fromJson(text, BaseMessage.class);
|
||||
|
||||
if ("gameStateUpdate".equals(baseMessage.type)) {
|
||||
currentGameState = gson.fromJson(text, GameStateResponse.class);
|
||||
Log.i(TAG, "WebSocket onMessage: GameStateUpdate reçu. Tour: " + currentGameState.getCurrentPlayerId());
|
||||
runOnUiThread(() -> {
|
||||
showLoading(false);
|
||||
updateMultiplayerUI(currentGameState);
|
||||
if (currentGameState.isGameOver()) {
|
||||
handleGameOverUI(currentGameState);
|
||||
}
|
||||
});
|
||||
} else if ("error".equals(baseMessage.type)) {
|
||||
ErrorMessage errorMsg = gson.fromJson(text, ErrorMessage.class);
|
||||
Log.e(TAG, "WebSocket onMessage: Erreur serveur reçue: " + errorMsg.message);
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(MultiplayerActivity.this, getString(R.string.server_error_prefix, errorMsg.message), Toast.LENGTH_LONG).show();
|
||||
statusTextMulti.setText(getString(R.string.server_error_prefix, errorMsg.message));
|
||||
});
|
||||
} else if ("info".equals(baseMessage.type)) {
|
||||
InfoMessage infoMsg = gson.fromJson(text, InfoMessage.class);
|
||||
Log.i(TAG, "WebSocket onMessage: Info serveur reçue: " + infoMsg.message);
|
||||
runOnUiThread(() -> Toast.makeText(MultiplayerActivity.this, "Info: " + infoMsg.message, Toast.LENGTH_SHORT).show());
|
||||
} else {
|
||||
Log.w(TAG, "WebSocket onMessage: Type de message inconnu reçu: " + baseMessage.type);
|
||||
}
|
||||
|
||||
} catch (JsonSyntaxException e) {
|
||||
Log.e(TAG, "WebSocket onMessage: Erreur parsing JSON", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(@NonNull WebSocket ws, @NonNull ByteString bytes) {
|
||||
super.onMessage(ws, bytes);
|
||||
Log.w(TAG, "WebSocket onMessage: Reçu message binaire (non géré)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosing(@NonNull WebSocket ws, int code, @NonNull String reason) {
|
||||
super.onClosing(ws, code, reason);
|
||||
Log.i(TAG, "WebSocket onClosing: Fermeture demandée par le serveur. Code=" + code + ", Raison=" + reason);
|
||||
isWebSocketConnected = false;
|
||||
ws.close(1000, null);
|
||||
runOnUiThread(() -> {
|
||||
statusTextMulti.setText(getString(R.string.websocket_closing));
|
||||
showLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(@NonNull WebSocket ws, int code, @NonNull String reason) {
|
||||
super.onClosed(ws, code, reason);
|
||||
Log.i(TAG, "WebSocket onClosed: Connexion fermée. Code=" + code + ", Raison=" + reason);
|
||||
isWebSocketConnected = false;
|
||||
webSocket = null;
|
||||
runOnUiThread(() -> {
|
||||
statusTextMulti.setText(getString(R.string.websocket_closed));
|
||||
showLoading(false);
|
||||
// Gérer la reconnexion si nécessaire
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull WebSocket ws, @NonNull Throwable t, @Nullable okhttp3.Response response) { // okhttp3.Response ici !
|
||||
super.onFailure(ws, t, response);
|
||||
Log.e(TAG, "WebSocket onFailure: Erreur de connexion.", t);
|
||||
isWebSocketConnected = false;
|
||||
webSocket = null;
|
||||
runOnUiThread(() -> {
|
||||
handleNetworkError(getString(R.string.error_websocket_connection));
|
||||
showLoading(false);
|
||||
// Gérer la reconnexion si nécessaire
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Data Classes internes pour WebSocket Messages ---
|
||||
private static class BaseMessage { String type; }
|
||||
private static class ErrorMessage extends BaseMessage { String message; }
|
||||
private static class InfoMessage extends BaseMessage { String message; }
|
||||
private static class RegisterMessage extends BaseMessage { String playerId; String gameId;
|
||||
RegisterMessage(String type, String pId, String gId){ this.type=type; this.playerId=pId; this.gameId=gId; }
|
||||
}
|
||||
private static class MoveMessage extends BaseMessage { String direction; String playerId;
|
||||
MoveMessage(String type, String dir, String pId) {this.type=type; this.direction=dir; this.playerId=pId;}
|
||||
}
|
||||
|
||||
// --- UI Update ---
|
||||
/** Met à jour l'interface multijoueur. DOIT être appelée sur le thread UI. */
|
||||
private void updateMultiplayerUI(GameStateResponse state) {
|
||||
if (state == null) return;
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) { runOnUiThread(() -> updateMultiplayerUI(state)); return; }
|
||||
Log.d(TAG, "updateMultiplayerUI: Mise à jour sur thread UI.");
|
||||
|
||||
myScoreLabelMulti.setText(getString(R.string.multiplayer_my_score, state.getMyScore(myPlayerId)));
|
||||
opponentScoreLabelMulti.setText(getString(R.string.multiplayer_opponent_score, state.getOpponentScore(myPlayerId)));
|
||||
|
||||
boolean myTurn = myPlayerId != null && myPlayerId.equals(state.getCurrentPlayerId());
|
||||
turnIndicatorMulti.setText(myTurn ? R.string.multiplayer_turn_yours : R.string.multiplayer_turn_opponent);
|
||||
turnIndicatorMulti.setTextColor(ContextCompat.getColor(this, myTurn ? R.color.tile_16 : R.color.text_tile_low));
|
||||
|
||||
syncBoardViewMulti(state.getBoard());
|
||||
|
||||
if (!state.isGameOver()) {
|
||||
if (!myTurn) { statusTextMulti.setText(R.string.multiplayer_status_waiting_opponent); }
|
||||
else { statusTextMulti.setText(""); }
|
||||
}
|
||||
// Le message de fin est géré par handleGameOverUI
|
||||
}
|
||||
|
||||
/** Gère l'affichage de fin de partie. DOIT être appelée sur le thread UI. */
|
||||
private void handleGameOverUI(GameStateResponse finalState) {
|
||||
if (finalState == null || !finalState.isGameOver()) return;
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) { runOnUiThread(() -> handleGameOverUI(finalState)); return; }
|
||||
|
||||
Log.i(TAG, "handleGameOverUI: Partie terminée. Winner: " + finalState.getWinnerId());
|
||||
String endMessage;
|
||||
if ("DRAW".equals(finalState.getWinnerId())) { endMessage = getString(R.string.game_over_draw); }
|
||||
else if (myPlayerId != null && myPlayerId.equals(finalState.getWinnerId())) { endMessage = getString(R.string.game_over_won); }
|
||||
else if (opponentPlayerId != null && opponentPlayerId.equals(finalState.getWinnerId())) { endMessage = getString(R.string.game_over_lost); }
|
||||
else { endMessage = getString(R.string.game_over_generic); }
|
||||
|
||||
statusTextMulti.setText(endMessage);
|
||||
turnIndicatorMulti.setText("");
|
||||
closeWebSocket(); // Fermer le WebSocket à la fin de la partie
|
||||
}
|
||||
|
||||
|
||||
// --- Board Sync (inchangé) ---
|
||||
/** Synchronise la vue de la grille multijoueur avec un état de plateau donné. */
|
||||
private void syncBoardViewMulti(int[][] boardState) {
|
||||
if (boardState == null || boardState.length != BOARD_SIZE || boardState[0].length != BOARD_SIZE) { Log.e(TAG, "syncBoardViewMulti: État du plateau invalide."); return; }
|
||||
boardGridLayoutMulti.removeAllViews();
|
||||
tileViewsMulti = new TextView[BOARD_SIZE][BOARD_SIZE];
|
||||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||
// Ajout fond (peut être optimisé si le fond ne change jamais)
|
||||
View backgroundCell = new View(this);
|
||||
backgroundCell.setBackgroundResource(R.drawable.tile_background);
|
||||
backgroundCell.getBackground().setTintList(ContextCompat.getColorStateList(this, R.color.tile_empty));
|
||||
@ -257,25 +441,19 @@ public class MultiplayerActivity extends AppCompatActivity {
|
||||
bgParams.setMargins(margin, margin, margin, margin);
|
||||
backgroundCell.setLayoutParams(bgParams);
|
||||
boardGridLayoutMulti.addView(backgroundCell);
|
||||
|
||||
// Ajout tuile si valeur > 0
|
||||
int value = boardState[r][c];
|
||||
if (value > 0) {
|
||||
TextView tileView = createTileTextViewMulti(value, r, c); // Utilise helper adapté
|
||||
TextView tileView = createTileTextViewMulti(value, r, c);
|
||||
tileViewsMulti[r][c] = tileView;
|
||||
boardGridLayoutMulti.addView(tileView);
|
||||
} else {
|
||||
tileViewsMulti[r][c] = null;
|
||||
}
|
||||
} else { tileViewsMulti[r][c] = null; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Crée une TextView pour une tuile multijoueur (similaire à MainActivity). */
|
||||
/** Crée une TextView pour une tuile multijoueur. */
|
||||
private TextView createTileTextViewMulti(int value, int row, int col) {
|
||||
TextView tileTextView = new TextView(this);
|
||||
setTileStyleMulti(tileTextView, value); // Utilise helper de style adapté
|
||||
|
||||
setTileStyleMulti(tileTextView, value);
|
||||
GridLayout.LayoutParams params = new GridLayout.LayoutParams(GridLayout.spec(row, 1f), GridLayout.spec(col, 1f));
|
||||
params.width = 0; params.height = 0;
|
||||
int margin = (int) getResources().getDimension(R.dimen.tile_margin);
|
||||
@ -283,20 +461,26 @@ public class MultiplayerActivity extends AppCompatActivity {
|
||||
tileTextView.setLayoutParams(params);
|
||||
return tileTextView;
|
||||
}
|
||||
|
||||
/** Applique le style à une tuile (similaire à MainActivity). */
|
||||
/** Applique le style à une tuile. */
|
||||
private void setTileStyleMulti(TextView tileTextView, int value) {
|
||||
// Copier/Adapter la logique de setTileStyle de MainActivity ici
|
||||
tileTextView.setText(value > 0 ? String.valueOf(value) : "");
|
||||
tileTextView.setGravity(Gravity.CENTER);
|
||||
tileTextView.setTypeface(null, android.graphics.Typeface.BOLD);
|
||||
int backgroundColorId; int textColorId; int textSizeId;
|
||||
switch (value) {
|
||||
case 0: backgroundColorId = R.color.tile_empty; textColorId = android.R.color.transparent; textSizeId = R.dimen.text_size_tile_small; break;
|
||||
case 2: backgroundColorId = R.color.tile_2; textColorId = R.color.text_tile_low; textSizeId = R.dimen.text_size_tile_small; break;
|
||||
case 4: backgroundColorId = R.color.tile_4; textColorId = R.color.text_tile_low; textSizeId = R.dimen.text_size_tile_small; break;
|
||||
// ... autres cas ...
|
||||
default: backgroundColorId = R.color.tile_super; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break;
|
||||
case 2: backgroundColorId = R.color.tile_2; textColorId = R.color.text_tile_low; textSizeId = R.dimen.text_size_tile_small; break;
|
||||
case 4: backgroundColorId = R.color.tile_4; textColorId = R.color.text_tile_low; textSizeId = R.dimen.text_size_tile_small; break;
|
||||
case 8: backgroundColorId = R.color.tile_8; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break;
|
||||
case 16: backgroundColorId = R.color.tile_16; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break;
|
||||
case 32: backgroundColorId = R.color.tile_32; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break;
|
||||
case 64: backgroundColorId = R.color.tile_64; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break;
|
||||
case 128: backgroundColorId = R.color.tile_128; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break;
|
||||
case 256: backgroundColorId = R.color.tile_256; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break;
|
||||
case 512: backgroundColorId = R.color.tile_512; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break;
|
||||
case 1024: backgroundColorId = R.color.tile_1024; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break;
|
||||
case 2048: backgroundColorId = R.color.tile_2048; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break;
|
||||
default: backgroundColorId = R.color.tile_super; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break;
|
||||
}
|
||||
tileTextView.setBackgroundResource(R.drawable.tile_background);
|
||||
tileTextView.getBackground().setTint(ContextCompat.getColor(this, backgroundColorId));
|
||||
@ -304,6 +488,7 @@ public class MultiplayerActivity extends AppCompatActivity {
|
||||
tileTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(textSizeId));
|
||||
}
|
||||
|
||||
// --- Swipe Handling (Modifié pour envoyer via WebSocket) ---
|
||||
|
||||
/** Configure le listener de swipe pour le plateau multijoueur. */
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@ -316,71 +501,33 @@ public class MultiplayerActivity extends AppCompatActivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/** Gère un swipe dans le contexte multijoueur. */
|
||||
/** Gère un swipe : envoie un message WebSocket 'move'. */
|
||||
private void handleMultiplayerSwipe(Direction direction) {
|
||||
if (currentGameState == null || currentGameId == null || currentGameState.isGameOver()) {
|
||||
Log.d(TAG, "Swipe ignoré (jeu non prêt ou terminé).");
|
||||
return;
|
||||
}
|
||||
// Vérifie si c'est notre tour
|
||||
if (!myPlayerId.equals(currentGameState.getCurrentPlayerId())) {
|
||||
Log.d(TAG, "Swipe ignoré (pas notre tour).");
|
||||
Toast.makeText(this, "Ce n'est pas votre tour.", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (currentGameState == null || currentGameId == null || currentGameState.isGameOver() || myPlayerId == null) { Log.d(TAG, "Swipe ignoré (état invalide)."); return; }
|
||||
if (!myPlayerId.equals(currentGameState.getCurrentPlayerId())) { Toast.makeText(this, R.string.error_not_your_turn, Toast.LENGTH_SHORT).show(); return; }
|
||||
if (webSocket == null || !isWebSocketConnected) { Toast.makeText(this, R.string.error_websocket_disconnected, Toast.LENGTH_SHORT).show(); connectWebSocket(); return; }
|
||||
|
||||
Log.d(TAG, "Swipe détecté: " + direction + ". Envoi du mouvement...");
|
||||
showLoading(true); // Affiche indicateur pendant l'envoi
|
||||
statusTextMulti.setText("Envoi du mouvement...");
|
||||
stopPolling(); // Arrête le polling pendant qu'on joue notre coup
|
||||
Log.d(TAG, "Swipe détecté: " + direction + ". Envoi du mouvement via WebSocket...");
|
||||
statusTextMulti.setText(R.string.multiplayer_status_sending_move);
|
||||
|
||||
String directionString = direction.name(); // "UP", "DOWN", etc.
|
||||
MoveRequest moveRequest = new MoveRequest(directionString, myPlayerId);
|
||||
MoveMessage moveMsg = new MoveMessage("move", direction.name(), myPlayerId);
|
||||
String moveJson = gson.toJson(moveMsg);
|
||||
sendWebSocketMessage(moveJson);
|
||||
// L'UI sera mise à jour à la réception du gameStateUpdate
|
||||
}
|
||||
|
||||
apiService.makeMove(currentGameId, moveRequest).enqueue(new Callback<GameStateResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<GameStateResponse> call, @NonNull Response<GameStateResponse> response) {
|
||||
showLoading(false);
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
Log.i(TAG, "Mouvement envoyé avec succès.");
|
||||
currentGameState = response.body();
|
||||
updateMultiplayerUI(currentGameState); // Met à jour l'UI avec le nouvel état
|
||||
if (!currentGameState.isGameOver()) {
|
||||
startPolling(); // Redémarre le polling pour attendre l'adversaire
|
||||
} else {
|
||||
// Afficher message fin de partie (déjà fait dans updateUI)
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Erreur envoi mouvement: " + response.code() + " - " + response.message());
|
||||
handleNetworkError("Erreur lors de l'envoi du mouvement. Code: " + response.code());
|
||||
// Si erreur, on devrait peut-être retenter de récupérer l'état ?
|
||||
fetchGameState(); // Pour resynchroniser
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<GameStateResponse> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "Échec envoi mouvement", t);
|
||||
showLoading(false);
|
||||
handleNetworkError("Échec connexion pour envoi mouvement.");
|
||||
// Retenter de récupérer l'état ?
|
||||
fetchGameState();
|
||||
}
|
||||
// --- Utility Methods ---
|
||||
private void showLoading(boolean show) {
|
||||
if (loadingIndicatorMulti != null) { loadingIndicatorMulti.setVisibility(show ? View.VISIBLE : View.GONE); }
|
||||
}
|
||||
private void handleNetworkError(String message) {
|
||||
runOnUiThread(() -> {
|
||||
if (statusTextMulti != null) { statusTextMulti.setText(message); }
|
||||
Toast.makeText(MultiplayerActivity.this, message, Toast.LENGTH_LONG).show();
|
||||
showLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void showLoading(boolean show) {
|
||||
loadingIndicatorMulti.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void handleNetworkError(String message) {
|
||||
statusTextMulti.setText(message);
|
||||
// Optionnel: Afficher un Toast aussi
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
/** Énumération interne pour les directions (peut être partagée avec MainActivity). */
|
||||
private enum Direction { UP, DOWN, LEFT, RIGHT }
|
||||
|
||||
} // Fin MultiplayerActivity
|
@ -93,4 +93,30 @@
|
||||
<string name="settings_test_notif_inactivity">Test Inactivity Notif</string>
|
||||
<string name="sound_enabled">Sound effects enabled.</string>
|
||||
<string name="sound_disabled">Sound effects disabled.</string>
|
||||
|
||||
<string name="multiplayer_status_searching">Recherche d\'une partie…</string>
|
||||
<string name="multiplayer_status_found">Partie trouvée ! ID: %s…</string> <string name="multiplayer_status_connecting">Connexion au serveur…</string>
|
||||
<string name="multiplayer_status_waiting_state">Connecté. En attente de l\'état du jeu…</string>
|
||||
<string name="multiplayer_status_waiting_opponent">En attente du coup adverse…</string>
|
||||
<string name="multiplayer_status_sending_move">Envoi du mouvement…</string>
|
||||
|
||||
<string name="multiplayer_turn_yours">À Votre Tour</string>
|
||||
<string name="multiplayer_turn_opponent">Tour Adversaire</string>
|
||||
|
||||
<string name="multiplayer_my_score">Moi :\n%d</string>
|
||||
<string name="multiplayer_opponent_score">Autre :\n%d</string>
|
||||
<string name="error_join_create_game">Impossible de créer ou rejoindre (Code: %d)</string>
|
||||
<string name="error_server_connection">Échec de connexion au serveur.</string>
|
||||
<string name="error_websocket_connection">Erreur de connexion WebSocket.</string>
|
||||
<string name="error_websocket_disconnected">WebSocket déconnecté. Tentative de reconnexion…</string>
|
||||
<string name="error_not_your_turn">Ce n\'est pas votre tour.</string>
|
||||
<string name="server_error_prefix">Erreur Serveur: %s</string>
|
||||
<string name="websocket_connected">Connecté au serveur de jeu !</string>
|
||||
<string name="websocket_closing">Fermeture de la connexion…</string>
|
||||
<string name="websocket_closed">Connexion fermée.</string>
|
||||
|
||||
<string name="game_over_draw">Égalité !</string>
|
||||
<string name="game_over_won">Vous avez Gagné !</string>
|
||||
<string name="game_over_lost">Vous avez Perdu.</string>
|
||||
<string name="game_over_generic">Partie Terminée !</string>
|
||||
</resources>
|
@ -28,6 +28,7 @@ material = { group = "com.google.android.material", name = "material", version.r
|
||||
activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||
gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "gridlayout" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "loggingInterceptor" }
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
|
||||
[plugins]
|
||||
|
Loading…
x
Reference in New Issue
Block a user