diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7eeaabb..1b4be99 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -41,4 +41,8 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) + implementation(libs.retrofit) + implementation(libs.converter.gson) + implementation(libs.logging.interceptor) + implementation(libs.gson) } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/MainActivity.java b/app/src/main/java/legion/muyue/best2048/MainActivity.java index 065b53e..b493708 100644 --- a/app/src/main/java/legion/muyue/best2048/MainActivity.java +++ b/app/src/main/java/legion/muyue/best2048/MainActivity.java @@ -8,6 +8,8 @@ */ package legion.muyue.best2048; +import static androidx.core.content.ContextCompat.startActivity; + import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; @@ -1891,17 +1893,19 @@ public class MainActivity extends AppCompatActivity { // --- Placeholders Multi --- - /** - * Affiche une simple boîte de dialogue indiquant que la fonctionnalité multijoueur - * n'est pas encore disponible. Sert de placeholder pour le bouton "Multijoueur". - */ + /** Affiche l'écran du mode multijoueur (lance MultiplayerActivity). */ private void showMultiplayerScreen() { - Log.d(TAG, "Affichage dialogue placeholder Multijoueur."); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.multiplayer) - .setMessage("Fonctionnalité multijoueur à venir !") - .setPositiveButton(R.string.ok, null); // Bouton OK qui ferme simplement - builder.create().show(); + Log.d(TAG, "Affichage écran Multijoueur."); + Intent intent = new Intent(this, MultiplayerActivity.class); + // TODO: Passer des informations utiles à l'activité multi (ex: ID Joueur) + // intent.putExtra("playerId", myPlayerId); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.e(TAG,"MultiplayerActivity non trouvée ! Vérifier AndroidManifest.xml", e); + Toast.makeText(this, "Erreur: Fonctionnalité multijoueur non disponible.", Toast.LENGTH_SHORT).show(); + } + // AlertDialog retiré } // --- Sauvegarde / Chargement --- diff --git a/app/src/main/java/legion/muyue/best2048/MultiplayerActivity.java b/app/src/main/java/legion/muyue/best2048/MultiplayerActivity.java new file mode 100644 index 0000000..5a90bc6 --- /dev/null +++ b/app/src/main/java/legion/muyue/best2048/MultiplayerActivity.java @@ -0,0 +1,372 @@ +package legion.muyue.best2048; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +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.network.ApiClient; +import legion.muyue.best2048.network.ApiService; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +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; + private TextView myScoreLabelMulti; + private TextView opponentScoreLabelMulti; + private TextView turnIndicatorMulti; + private TextView statusTextMulti; + private ProgressBar loadingIndicatorMulti; + private TextView[][] tileViewsMulti = new TextView[BOARD_SIZE][BOARD_SIZE]; + + // Network + private ApiService apiService; + + // Game State + private String currentGameId = null; + private String myPlayerId = "Player1_Temp"; // TODO: Remplacer par une vraie identification + private String opponentPlayerId = null; + private GameStateResponse currentGameState = null; + + // Polling Handler + private Handler pollingHandler; + private Runnable pollingRunnable; + private boolean isPollingActive = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_multiplayer); + + findViewsMulti(); + apiService = ApiClient.getApiService(); + pollingHandler = new Handler(Looper.getMainLooper()); + + setupSwipeListenerMulti(); + initMultiplayerGame(); + } + + @Override + protected void onPause() { + super.onPause(); + stopPolling(); // Arrête le polling quand l'activité n'est plus visible + } + + @Override + protected void onResume() { + super.onResume(); + if (currentGameId != null && currentGameState != null && !currentGameState.isGameOver()) { + startPolling(); // Redémarre le polling si une partie est en cours + } + } + + private void findViewsMulti() { + boardGridLayoutMulti = findViewById(R.id.gameBoardMulti); + myScoreLabelMulti = findViewById(R.id.myScoreLabelMulti); + opponentScoreLabelMulti = findViewById(R.id.opponentScoreLabelMulti); + turnIndicatorMulti = findViewById(R.id.turnIndicatorMulti); + statusTextMulti = findViewById(R.id.multiplayerStatusText); + loadingIndicatorMulti = findViewById(R.id.loadingIndicatorMulti); + } + + /** Tente de créer ou rejoindre une partie multijoueur. */ + private void initMultiplayerGame() { + showLoading(true); + statusTextMulti.setText("Recherche d'une partie..."); + // TODO: Implémenter la logique d'obtention de myPlayerId + // Appel API pour créer/rejoindre + apiService.createOrJoinGame(myPlayerId).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + GameInfo gameInfo = response.body(); + currentGameId = gameInfo.getGameId(); + // TODO: Déterminer opponentPlayerId basé sur gameInfo.getPlayer1Id/getPlayer2Id + opponentPlayerId = myPlayerId.equals(gameInfo.getPlayer1Id()) ? gameInfo.getPlayer2Id() : gameInfo.getPlayer1Id(); + + Log.i(TAG, "Partie rejointe/créée: ID=" + currentGameId + ", Adversaire=" + opponentPlayerId); + statusTextMulti.setText("Partie trouvée ! ID: " + currentGameId); + fetchGameState(); // Récupère l'état initial + } else { + Log.e(TAG, "Erreur création/rejoindre partie: " + response.code()); + handleNetworkError("Impossible de créer ou rejoindre une partie."); + } + } + + @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."); + } + }); + } + + /** Récupère l'état actuel du jeu depuis le serveur. */ + private void fetchGameState() { + if (currentGameId == null) return; + // Ne montre le chargement que si pas déjà en polling (pour fluidité) + if(!isPollingActive) showLoading(true); + + apiService.getGameState(currentGameId).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + showLoading(false); + if (response.isSuccessful() && response.body() != null) { + 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 + } + } 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..."); + } + } + + @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(); + } + }); + } + + /** 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 + } + + + /** 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."); + return; + } + // Logique similaire à syncBoardView de MainActivity + boardGridLayoutMulti.removeAllViews(); // Simplification par reset complet + 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)); + GridLayout.LayoutParams bgParams = new GridLayout.LayoutParams(GridLayout.spec(r, 1f), GridLayout.spec(c, 1f)); + bgParams.width = 0; bgParams.height = 0; + int margin = (int) getResources().getDimension(R.dimen.tile_margin); + 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é + tileViewsMulti[r][c] = tileView; + boardGridLayoutMulti.addView(tileView); + } else { + tileViewsMulti[r][c] = null; + } + } + } + } + + /** Crée une TextView pour une tuile multijoueur (similaire à MainActivity). */ + private TextView createTileTextViewMulti(int value, int row, int col) { + TextView tileTextView = new TextView(this); + setTileStyleMulti(tileTextView, value); // Utilise helper de style adapté + + 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); + params.setMargins(margin, margin, margin, margin); + tileTextView.setLayoutParams(params); + return tileTextView; + } + + /** Applique le style à une tuile (similaire à MainActivity). */ + 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; + } + tileTextView.setBackgroundResource(R.drawable.tile_background); + tileTextView.getBackground().setTint(ContextCompat.getColor(this, backgroundColorId)); + tileTextView.setTextColor(ContextCompat.getColor(this, textColorId)); + tileTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(textSizeId)); + } + + + /** Configure le listener de swipe pour le plateau multijoueur. */ + @SuppressLint("ClickableViewAccessibility") + private void setupSwipeListenerMulti() { + boardGridLayoutMulti.setOnTouchListener(new OnSwipeTouchListener(this, new OnSwipeTouchListener.SwipeListener() { + @Override public void onSwipeTop() { handleMultiplayerSwipe(Direction.UP); } + @Override public void onSwipeBottom() { handleMultiplayerSwipe(Direction.DOWN); } + @Override public void onSwipeLeft() { handleMultiplayerSwipe(Direction.LEFT); } + @Override public void onSwipeRight() { handleMultiplayerSwipe(Direction.RIGHT); } + })); + } + + /** Gère un swipe dans le contexte multijoueur. */ + 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; + } + + 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 + + String directionString = direction.name(); // "UP", "DOWN", etc. + MoveRequest moveRequest = new MoveRequest(directionString, myPlayerId); + + 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(); + } + }); + } + + + 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/java/legion/muyue/best2048/data/GameInfo.java b/app/src/main/java/legion/muyue/best2048/data/GameInfo.java new file mode 100644 index 0000000..4d8b4a0 --- /dev/null +++ b/app/src/main/java/legion/muyue/best2048/data/GameInfo.java @@ -0,0 +1,20 @@ +package legion.muyue.best2048.data; // Créez un sous-package data si vous voulez + +import com.google.gson.annotations.SerializedName; + +public class GameInfo { + @SerializedName("gameId") // Correspond au nom du champ JSON + private String gameId; + @SerializedName("status") // Ex: WAITING, PLAYING, FINISHED + private String status; + @SerializedName("player1Id") + private String player1Id; + @SerializedName("player2Id") + private String player2Id; // Peut être null si en attente + + // --- Getters (et Setters si nécessaire) --- + public String getGameId() { return gameId; } + public String getStatus() { return status; } + public String getPlayer1Id() { return player1Id; } + public String getPlayer2Id() { return player2Id; } +} \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/data/GameStateResponse.java b/app/src/main/java/legion/muyue/best2048/data/GameStateResponse.java new file mode 100644 index 0000000..767bd1a --- /dev/null +++ b/app/src/main/java/legion/muyue/best2048/data/GameStateResponse.java @@ -0,0 +1,47 @@ +package legion.muyue.best2048.data; + +import com.google.gson.annotations.SerializedName; + +public class GameStateResponse { + @SerializedName("gameId") + private String gameId; + @SerializedName("board") + private int[][] board; // Plateau de jeu actuel + @SerializedName("player1Score") + private int player1Score; + @SerializedName("player2Score") + private int player2Score; + @SerializedName("currentPlayerId") // ID du joueur dont c'est le tour + private String currentPlayerId; + @SerializedName("isGameOver") + private boolean isGameOver; + @SerializedName("winnerId") // ID du gagnant si terminé, null sinon + private String winnerId; + @SerializedName("status") + private String status; + + // --- Getters --- + public String getGameId() { return gameId; } + public int[][] getBoard() { return board; } + public int getPlayer1Score() { return player1Score; } + public int getPlayer2Score() { return player2Score; } + public String getCurrentPlayerId() { return currentPlayerId; } + public boolean isGameOver() { return isGameOver; } + public String getWinnerId() { return winnerId; } + public String getStatus() { return status; } + + // --- Méthode utilitaire pour obtenir le score de l'adversaire --- + public int getOpponentScore(String myPlayerId) { + if (myPlayerId == null) return 0; + // TODO: Logique pour déterminer qui est player1/player2 basée sur l'API + // Supposons pour l'instant que player1 est l'hôte, player2 l'invité + // Et que l'API nous donne l'ID de player1/player2 dans un autre champ (ex: GameInfo) + // Placeholder: + return (myPlayerId.equals("player1_placeholder")) ? player2Score : player1Score; + } + public int getMyScore(String myPlayerId) { + if (myPlayerId == null) return 0; + // Placeholder: + return (myPlayerId.equals("player1_placeholder")) ? player1Score : player2Score; + } +} \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/data/MoveRequest.java b/app/src/main/java/legion/muyue/best2048/data/MoveRequest.java new file mode 100644 index 0000000..8cc5e3c --- /dev/null +++ b/app/src/main/java/legion/muyue/best2048/data/MoveRequest.java @@ -0,0 +1,12 @@ +package legion.muyue.best2048.data; + +public class MoveRequest { + private String direction; // "UP", "DOWN", "LEFT", "RIGHT" + private String playerId; // ID du joueur qui fait le mouvement + + public MoveRequest(String direction, String playerId) { + this.direction = direction; + this.playerId = playerId; + } + // Pas besoin de getters si seulement utilisé pour l'envoi avec Gson +} \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/network/ApiClient.java b/app/src/main/java/legion/muyue/best2048/network/ApiClient.java new file mode 100644 index 0000000..ed3e2e7 --- /dev/null +++ b/app/src/main/java/legion/muyue/best2048/network/ApiClient.java @@ -0,0 +1,49 @@ +package legion.muyue.best2048.network; + +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ApiClient { + + // URL de base de votre API serveur + private static final String BASE_URL = "http://best2048.legion-muyue.fr/api/"; // Assurez-vous que le chemin est correct + + private static Retrofit retrofit = null; + + /** + * Crée et retourne une instance singleton de Retrofit configurée. + * Inclut un intercepteur pour logger les requêtes/réponses HTTP (utile pour le debug). + * + * @return L'instance configurée de Retrofit. + */ + public static Retrofit getClient() { + if (retrofit == null) { + // Intercepteur pour voir les logs HTTP dans Logcat (Niveau BODY pour tout voir) + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); + logging.setLevel(HttpLoggingInterceptor.Level.BODY); + + // Client OkHttp avec l'intercepteur + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(logging) + .build(); + + // Construction de l'instance Retrofit + retrofit = new Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) // Utilise le client OkHttp configuré + .addConverterFactory(GsonConverterFactory.create()) // Utilise Gson pour parser le JSON + .build(); + } + return retrofit; + } + + /** + * Fournit une instance de l'interface ApiService. + * @return Instance de ApiService. + */ + public static ApiService getApiService() { + return getClient().create(ApiService.class); + } +} \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/network/ApiService.java b/app/src/main/java/legion/muyue/best2048/network/ApiService.java new file mode 100644 index 0000000..0ba0713 --- /dev/null +++ b/app/src/main/java/legion/muyue/best2048/network/ApiService.java @@ -0,0 +1,41 @@ +package legion.muyue.best2048.network; // Créez un sous-package network + +import legion.muyue.best2048.data.GameInfo; +import legion.muyue.best2048.data.GameStateResponse; +import legion.muyue.best2048.data.MoveRequest; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; // Pour éventuels paramètres de création + +public interface ApiService { + + /** + * Crée une nouvelle partie ou rejoint une partie en attente (matchmaking simple). + * TODO: Définir les paramètres nécessaires (ex: ID du joueur). + * @return Informations sur la partie créée/rejointe. + */ + @POST("games") // Endpoint: /api/games (POST) + Call createOrJoinGame(@Query("playerId") String playerId); // Exemple avec ID joueur en query param + + /** + * Récupère l'état actuel complet d'une partie spécifique. + * @param gameId L'identifiant unique de la partie. + * @return L'état actuel du jeu. + */ + @GET("games/{gameId}") // Endpoint: /api/games/{gameId} (GET) + Call getGameState(@Path("gameId") String gameId); + + /** + * Soumet le mouvement d'un joueur pour une partie spécifique. + * Le serveur validera si c'est bien le tour de ce joueur. + * @param gameId L'identifiant unique de la partie. + * @param moveRequest L'objet contenant la direction du mouvement et l'ID du joueur. + * @return Le nouvel état du jeu après application du mouvement (ou un message d'erreur). + */ + @POST("games/{gameId}/moves") // Endpoint: /api/games/{gameId}/moves (POST) + Call makeMove(@Path("gameId") String gameId, @Body MoveRequest moveRequest); + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_multiplayer.xml b/app/src/main/res/layout/activity_multiplayer.xml new file mode 100644 index 0000000..5a06c72 --- /dev/null +++ b/app/src/main/res/layout/activity_multiplayer.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ab8db03..784c4e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,26 +2,33 @@ activityVersion = "26" agp = "8.9.1" androidxActivity = "1.9.0" +gson = "2.10.1" junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1" appcompat = "1.6.1" +loggingInterceptor = "4.9.3" material = "1.10.0" activity = "1.8.0" constraintlayout = "2.1.4" gridlayout = "1.0.0" +retrofit = "2.9.0" [libraries] activity-v190 = { module = "androidx.activity:activity", version.ref = "androidxActivity" } activity-v26 = { module = "androidx.activity:activity", version.ref = "activityVersion" } +converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } junit = { group = "junit", name = "junit", version.ref = "junit" } ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } 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" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }