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:
parent
bee192e335
commit
1977d2de3f
@ -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)
|
||||
}
|
@ -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 ---
|
||||
|
372
app/src/main/java/legion/muyue/best2048/MultiplayerActivity.java
Normal file
372
app/src/main/java/legion/muyue/best2048/MultiplayerActivity.java
Normal 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
|
20
app/src/main/java/legion/muyue/best2048/data/GameInfo.java
Normal file
20
app/src/main/java/legion/muyue/best2048/data/GameInfo.java
Normal 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; }
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
96
app/src/main/res/layout/activity_multiplayer.xml
Normal file
96
app/src/main/res/layout/activity_multiplayer.xml
Normal 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>
|
@ -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" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user