diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1b4be99..e24fdf9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -45,4 +45,5 @@ dependencies { implementation(libs.converter.gson) implementation(libs.logging.interceptor) implementation(libs.gson) + implementation(libs.okhttp) } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/MultiplayerActivity.java b/app/src/main/java/legion/muyue/best2048/MultiplayerActivity.java index 22d327a..3f19463 100644 --- a/app/src/main/java/legion/muyue/best2048/MultiplayerActivity.java +++ b/app/src/main/java/legion/muyue/best2048/MultiplayerActivity.java @@ -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() { @Override + // Utilise bien retrofit2.Response ici public void onResponse(@NonNull Call call, @NonNull Response 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 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() { @Override + // Utilise bien retrofit2.Response ici public void onResponse(@NonNull Call call, @NonNull Response 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 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() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response 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 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 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2f4214a..f5666e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,4 +93,30 @@ Test Inactivity Notif Sound effects enabled. Sound effects disabled. + + Recherche d\'une partie… + Partie trouvée ! ID: %s… Connexion au serveur… + Connecté. En attente de l\'état du jeu… + En attente du coup adverse… + Envoi du mouvement… + + À Votre Tour + Tour Adversaire + + Moi :\n%d + Autre :\n%d + Impossible de créer ou rejoindre (Code: %d) + Échec de connexion au serveur. + Erreur de connexion WebSocket. + WebSocket déconnecté. Tentative de reconnexion… + Ce n\'est pas votre tour. + Erreur Serveur: %s + Connecté au serveur de jeu ! + Fermeture de la connexion… + Connexion fermée. + + Égalité ! + Vous avez Gagné ! + Vous avez Perdu. + Partie Terminée ! \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 784c4e1..dd2a02b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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]