Feat: Structure de base pour le mode Multijoueur (Client)

- Ajout dépendances Retrofit, OkHttp logging, Gson dans build.gradle.
- Création des modèles de données (POJO) pour l'API: GameInfo, GameStateResponse, MoveRequest.
- Création de l'interface Retrofit 'ApiService' définissant les endpoints (create/join, get state, make move).
- Création du client 'ApiClient' pour configurer et fournir l'instance Retrofit.
- Création de 'MultiplayerActivity' et 'activity_multiplayer.xml' pour l'écran de jeu multi.
- Implémentation de base dans MultiplayerActivity:
  - Initialisation (findViews, ApiService).
  - Tentative de création/rejoindre une partie via API.
  - Récupération de l'état initial du jeu via API (fetchGameState).
  - Mise à jour basique de l'UI multijoueur (scores, tour, plateau via syncBoardViewMulti).
  - Gestion basique des swipes (handleMultiplayerSwipe) : vérification du tour, envoi du mouvement via API.
  - Implémentation d'un polling simple et inefficace pour récupérer les coups adverses.
  - Gestion basique des erreurs réseau et indicateur de chargement.
- Modification de MainActivity pour lancer MultiplayerActivity via le bouton 'Multijoueur'.
This commit is contained in:
Augustin ROUX 2025-04-04 18:54:51 +02:00
parent bee192e335
commit 1977d2de3f
10 changed files with 662 additions and 10 deletions

View File

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

View File

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

View File

@ -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<GameInfo>() {
@Override
public void onResponse(@NonNull Call<GameInfo> call, @NonNull Response<GameInfo> 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<GameInfo> 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<GameStateResponse>() {
@Override
public void onResponse(@NonNull Call<GameStateResponse> call, @NonNull Response<GameStateResponse> 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<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();
}
});
}
/** 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<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();
}
});
}
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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<GameInfo> 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<GameStateResponse> 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<GameStateResponse> makeMove(@Path("gameId") String gameId, @Body MoveRequest moveRequest);
}

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/multiplayer_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_color"
tools:context=".MultiplayerActivity">
<LinearLayout
android:id="@+id/multiplayerInfoPanel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/padding_general"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/myScoreLabelMulti"
style="@style/ScoreLabelStyle"
android:layout_weight="1"
android:layout_marginEnd="@dimen/margin_between_elements"
android:text="Mon Score:\n0" />
<TextView
android:id="@+id/turnIndicatorMulti"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:paddingStart="8dp" android:paddingEnd="8dp"
android:textColor="@color/text_tile_low"
android:textStyle="bold"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="À votre tour"/>
<TextView
android:id="@+id/opponentScoreLabelMulti"
style="@style/ScoreLabelStyle"
android:layout_weight="1"
android:layout_marginStart="@dimen/margin_between_elements"
android:text="Adversaire:\n0" />
</LinearLayout>
<androidx.cardview.widget.CardView
android:id="@+id/gameBoardContainerMulti"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="@dimen/padding_general"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="@dimen/corner_radius"
app:cardElevation="0dp"
app:layout_constraintTop_toBottomOf="@id/multiplayerInfoPanel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toTopOf="@+id/multiplayerStatusText">
<androidx.gridlayout.widget.GridLayout
android:id="@+id/gameBoardMulti"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/game_board_background"
android:padding="@dimen/padding_game_board"
app:columnCount="4"
app:rowCount="4" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/multiplayerStatusText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="@dimen/padding_general"
android:gravity="center"
android:textAppearance="?android:attr/textAppearanceMedium"
app:layout_constraintTop_toBottomOf="@id/gameBoardContainerMulti"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="En attente d'un adversaire..."/>
<ProgressBar
android:id="@+id/loadingIndicatorMulti"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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