diff --git a/android/src/main/java/com/group14/regicidechess/android/AndroidFirebase.java b/android/src/main/java/com/group14/regicidechess/android/AndroidFirebase.java new file mode 100644 index 0000000..efd296d --- /dev/null +++ b/android/src/main/java/com/group14/regicidechess/android/AndroidFirebase.java @@ -0,0 +1,358 @@ +package com.group14.regicidechess.android; + +import android.os.Handler; +import android.os.Looper; + +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.ValueEventListener; +import com.group14.regicidechess.API; +import com.group14.regicidechess.database.FirebaseAPI; +import com.group14.regicidechess.model.Lobby; +import com.group14.regicidechess.model.Move; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AndroidFirebase implements FirebaseAPI, API { + + private final DatabaseReference db = FirebaseDatabase.getInstance().getReference(); + + // ── API (Legacy) ────────────────────────────────────────────────────────── + + @Override + public void createLobby() { + db.child("lobbies").push().setValue(1); + } + + // ── Lobby ───────────────────────────────────────────────────────────────── + + @Override + public void createLobby(Lobby lobby, Callback onSuccess, Callback onError) { + String gameId = lobby.getGameId(); + Map data = new HashMap<>(); + data.put("boardSize", lobby.getBoardSize()); + data.put("budget", lobby.getBudget()); + data.put("status", "waiting"); + + db.child("lobbies").child(gameId).setValue(data) + .addOnSuccessListener(v -> onSuccess.call(gameId)) + .addOnFailureListener(e -> onError.call(e.getMessage())); + } + + @Override + public void fetchLobby(String gameId, Callback onSuccess, Callback onError) { + db.child("lobbies").child(gameId).get() + .addOnSuccessListener(snapshot -> { + if (!snapshot.exists()) { onError.call("Lobby not found"); return; } + int boardSize = getInt(snapshot, "boardSize"); + int budget = getInt(snapshot, "budget"); + onSuccess.call(new Lobby(gameId, boardSize, budget, + System.currentTimeMillis() + 30 * 60 * 1000L)); + }) + .addOnFailureListener(e -> onError.call(e.getMessage())); + } + + @Override + public void joinLobby(String gameId, Callback onSuccess, Callback onError) { + db.child("lobbies").child(gameId).get() + .addOnSuccessListener(snapshot -> { + if (!snapshot.exists()) { onError.call("Lobby not found"); return; } + int boardSize = getInt(snapshot, "boardSize"); + int budget = getInt(snapshot, "budget"); + db.child("lobbies").child(gameId).child("status").setValue("joined"); + onSuccess.call(new Lobby(gameId, boardSize, budget, + System.currentTimeMillis() + 30 * 60 * 1000L)); + }) + .addOnFailureListener(e -> onError.call(e.getMessage())); + } + + @Override + public void listenForOpponentReady(String gameId, Runnable onReady) { + db.child("lobbies").child(gameId).child("status") + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if ("joined".equals(snapshot.getValue(String.class))) { + snapshot.getRef().removeEventListener(this); + onReady.run(); + } + } + @Override public void onCancelled(DatabaseError e) {} + }); + } + + @Override + public void startGame(String gameId) { + db.child("lobbies").child(gameId).child("status").setValue("started"); + } + + @Override + public void listenForGameStart(String gameId, Runnable onStart) { + db.child("lobbies").child(gameId).child("status") + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if ("started".equals(snapshot.getValue(String.class))) { + snapshot.getRef().removeEventListener(this); + onStart.run(); + } + } + @Override public void onCancelled(DatabaseError e) {} + }); + } + + // ── Setup ───────────────────────────────────────────────────────────────── + + @Override + public void confirmSetup(String gameId, boolean isWhite, int[][] board, Runnable onSuccess) { + String player = isWhite ? "white" : "black"; + DatabaseReference gameRef = db.child("games").child(gameId); + + List> pieces = new ArrayList<>(); + for (int col = 0; col < board.length; col++) { + for (int row = 0; row < board[col].length; row++) { + if (board[col][row] != 0) { + Map entry = new HashMap<>(); + entry.put("col", col); + entry.put("row", row); + entry.put("piece", board[col][row]); + pieces.add(entry); + } + } + } + + gameRef.child("boards").child(player).setValue(pieces) + .addOnSuccessListener(v1 -> + gameRef.child("setup").child(player).setValue("ready") + .addOnSuccessListener(v2 -> + gameRef.child("setup").addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + boolean whiteReady = "ready".equals(snapshot.child("white").getValue(String.class)); + boolean blackReady = "ready".equals(snapshot.child("black").getValue(String.class)); + if (whiteReady && blackReady) { + gameRef.child("bothReady").setValue(true); + } + onSuccess.run(); + } + @Override public void onCancelled(DatabaseError e) { onSuccess.run(); } + }) + ) + ) + .addOnFailureListener(e -> onSuccess.run()); + } + + @Override + public void getOpponentBoard(String gameId, boolean localIsWhite, Callback onBoard) { + String opponentKey = localIsWhite ? "black" : "white"; + db.child("games").child(gameId).child("boards").child(opponentKey).get() + .addOnSuccessListener(snapshot -> { + if (!snapshot.exists()) { onBoard.call(new int[0][0]); return; } + int maxCol = 0, maxRow = 0; + for (DataSnapshot entry : snapshot.getChildren()) { + int c = getInt(entry, "col"), r = getInt(entry, "row"); + if (c > maxCol) maxCol = c; + if (r > maxRow) maxRow = r; + } + int size = Math.max(maxCol, maxRow) + 1; + int[][] b = new int[size][size]; + for (DataSnapshot entry : snapshot.getChildren()) { + b[getInt(entry, "col")][getInt(entry, "row")] = getInt(entry, "piece"); + } + onBoard.call(b); + }) + .addOnFailureListener(e -> onBoard.call(new int[0][0])); + } + + @Override + public void listenForBothSetupReady(String gameId, Runnable onBothReady) { + db.child("games").child(gameId).child("bothReady") + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Boolean bothReady = snapshot.getValue(Boolean.class); + if (bothReady != null && bothReady) { + snapshot.getRef().removeEventListener(this); + onBothReady.run(); + } + } + @Override public void onCancelled(DatabaseError e) {} + }); + } + + // ── Moves ───────────────────────────────────────────────────────────────── + + @Override + public void saveMove(String gameId, Move move, Runnable onSuccess) { + Map data = new HashMap<>(); + data.put("fromCol", (int) move.getFrom().x); + data.put("fromRow", (int) move.getFrom().y); + data.put("toCol", (int) move.getTo().x); + data.put("toRow", (int) move.getTo().y); + data.put("player", move.getPlayer().isWhite() ? "white" : "black"); + data.put("timestamp", System.currentTimeMillis()); + if (move.getPromotion() != null) { + data.put("promotion", move.getPromotion()); + } + + db.child("games").child(gameId).child("moves").push() + .setValue(data) + .addOnSuccessListener(v -> onSuccess.run()); + } + + @Override + public void listenForOpponentMove(String gameId, Callback onMove) { + // Only listen for moves added AFTER we attach the listener. + // We do this by first reading the last known key, then attaching + // a child listener that skips everything up to and including that key. + db.child("games").child(gameId).child("moves") + .orderByKey().limitToLast(1) + .get() + .addOnSuccessListener(snapshot -> { + // Collect the key of the last existing move (may be null if none) + final String[] lastExistingKey = { null }; + for (DataSnapshot child : snapshot.getChildren()) { + lastExistingKey[0] = child.getKey(); + } + + db.child("games").child(gameId).child("moves") + .addChildEventListener(new ChildEventListener() { + private boolean seenStart = (lastExistingKey[0] == null); + + @Override + public void onChildAdded(DataSnapshot snapshot, String prev) { + // Skip all pre-existing moves until we pass the last known key + if (!seenStart) { + if (snapshot.getKey().equals(lastExistingKey[0])) { + seenStart = true; + } + return; + } + + int fromCol = getInt(snapshot, "fromCol"); + int fromRow = getInt(snapshot, "fromRow"); + int toCol = getInt(snapshot, "toCol"); + int toRow = getInt(snapshot, "toRow"); + String mover = snapshot.child("player").getValue(String.class); + int isWhite = "white".equals(mover) ? 1 : 0; + + String promo = snapshot.child("promotion").getValue(String.class); + int promoCode = 0; + if (promo != null) { + if ("Queen".equals(promo)) promoCode = 2; + else if ("Rook".equals(promo)) promoCode = 3; + else if ("Bishop".equals(promo)) promoCode = 4; + else if ("Knight".equals(promo)) promoCode = 5; + } + + onMove.call(new int[]{fromCol, fromRow, toCol, toRow, isWhite, promoCode}); + } + @Override public void onChildChanged(DataSnapshot s, String p) {} + @Override public void onChildRemoved(DataSnapshot s) {} + @Override public void onChildMoved (DataSnapshot s, String p) {} + @Override public void onCancelled (DatabaseError e) {} + }); + }) + .addOnFailureListener(e -> { + // Fallback: attach listener without filtering (original behaviour) + db.child("games").child(gameId).child("moves") + .addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String prev) { + int fromCol = getInt(snapshot, "fromCol"); + int fromRow = getInt(snapshot, "fromRow"); + int toCol = getInt(snapshot, "toCol"); + int toRow = getInt(snapshot, "toRow"); + String mover = snapshot.child("player").getValue(String.class); + int isWhite = "white".equals(mover) ? 1 : 0; + onMove.call(new int[]{fromCol, fromRow, toCol, toRow, isWhite, 0}); + } + @Override public void onChildChanged(DataSnapshot s, String p) {} + @Override public void onChildRemoved(DataSnapshot s) {} + @Override public void onChildMoved (DataSnapshot s, String p) {} + @Override public void onCancelled (DatabaseError e) {} + }); + }); + } + + // ── Heartbeat ───────────────────────────────────────────────────────────── + + @Override + public void sendHeartbeat(String gameId, boolean isWhite) { + db.child("games").child(gameId).child("heartbeat") + .child(isWhite ? "white" : "black") + .setValue(System.currentTimeMillis()); + } + + @Override + public void listenForHeartbeat(String gameId, boolean listenForWhite, + long timeoutMs, Callback onHeartbeat, Runnable onTimeout) { + String player = listenForWhite ? "white" : "black"; + Handler handler = new Handler(Looper.getMainLooper()); + Runnable timeoutRunnable = onTimeout::run; + + // Start timeout timer — reset each time a valid heartbeat arrives + handler.postDelayed(timeoutRunnable, timeoutMs); + + db.child("games").child(gameId).child("heartbeat").child(player) + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Long timestamp = snapshot.getValue(Long.class); + if (timestamp == null) return; // No heartbeat yet — let timer run + + long now = System.currentTimeMillis(); + long latency = now - timestamp; + + // Ignore stale timestamps (e.g. from a previous game session) + if (latency < 0 || latency > timeoutMs) return; + + // Valid heartbeat — reset the timeout and report latency + handler.removeCallbacks(timeoutRunnable); + handler.postDelayed(timeoutRunnable, timeoutMs); + onHeartbeat.call(latency); + } + @Override public void onCancelled(DatabaseError e) {} + }); + } + + // ── Game over ───────────────────────────────────────────────────────────── + + @Override + public void signalGameOver(String gameId, String reason) { + db.child("games").child(gameId).child("gameOver").setValue(reason); + } + + @Override + public void listenForGameOver(String gameId, Callback onGameOver) { + db.child("games").child(gameId).child("gameOver") + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + String reason = snapshot.getValue(String.class); + if (reason != null) { + snapshot.getRef().removeEventListener(this); + onGameOver.call(reason); + } + } + @Override public void onCancelled(DatabaseError e) {} + }); + } + + // ───────────────────────────────────────────────────────────────────────── + // Helpers + // ───────────────────────────────────────────────────────────────────────── + + private int getInt(DataSnapshot snapshot, String key) { + Object val = snapshot.child(key).getValue(); + if (val instanceof Long) return ((Long) val).intValue(); + if (val instanceof Integer) return (Integer) val; + return 0; + } +} \ No newline at end of file diff --git a/android/src/main/java/com/group14/regicidechess/android/AndroidLauncher.java b/android/src/main/java/com/group14/regicidechess/android/AndroidLauncher.java index 8a8371a..6a9fcde 100644 --- a/android/src/main/java/com/group14/regicidechess/android/AndroidLauncher.java +++ b/android/src/main/java/com/group14/regicidechess/android/AndroidLauncher.java @@ -5,15 +5,20 @@ import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; import com.group14.regicidechess.Main; +import com.group14.regicidechess.database.DatabaseManager; /** Launches the Android application. */ public class AndroidLauncher extends AndroidApplication { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - AndroidApplicationConfiguration configuration = new AndroidApplicationConfiguration(); - configuration.useImmersiveMode = true; // Recommended, but not required. - initialize(new Main(new AndroidAPI()), configuration); + // Initialise Firebase bridge before starting the game + AndroidFirebase firebase = new AndroidFirebase(); + DatabaseManager.getInstance().init(firebase); + + AndroidApplicationConfiguration configuration = new AndroidApplicationConfiguration(); + configuration.useImmersiveMode = true; + initialize(new Main(firebase), configuration); } -} +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/Main.java b/core/src/main/java/com/group14/regicidechess/Main.java index 6d2d775..7482b99 100644 --- a/core/src/main/java/com/group14/regicidechess/Main.java +++ b/core/src/main/java/com/group14/regicidechess/Main.java @@ -2,7 +2,7 @@ import com.badlogic.gdx.Game; import com.badlogic.gdx.graphics.g2d.SpriteBatch; -import com.group14.regicidechess.screens.MainMenuScreen; +import com.group14.regicidechess.screens.mainmenu.MainMenuScreen; import com.group14.regicidechess.utils.ResourceManager; public class Main extends Game { diff --git a/core/src/main/java/com/group14/regicidechess/database/DatabaseManager.java b/core/src/main/java/com/group14/regicidechess/database/DatabaseManager.java new file mode 100644 index 0000000..9a84a45 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/database/DatabaseManager.java @@ -0,0 +1,33 @@ +package com.group14.regicidechess.database; + +public class DatabaseManager { + + private static DatabaseManager instance; + private FirebaseAPI api; + + private DatabaseManager() {} + + public static DatabaseManager getInstance() { + if (instance == null) instance = new DatabaseManager(); + return instance; + } + + public void init(FirebaseAPI api) { + this.api = api; + } + + /** + * Returns the FirebaseAPI implementation. + * Throws a clear IllegalStateException if init() was never called, + * so the cause is obvious instead of a confusing NullPointerException. + */ + public FirebaseAPI getApi() { + if (api == null) { + throw new IllegalStateException( + "DatabaseManager not initialised! " + + "Call DatabaseManager.getInstance().init(firebase) " + + "in your launcher before starting the game."); + } + return api; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/database/FirebaseAPI.java b/core/src/main/java/com/group14/regicidechess/database/FirebaseAPI.java new file mode 100644 index 0000000..140946c --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/database/FirebaseAPI.java @@ -0,0 +1,82 @@ +package com.group14.regicidechess.database; + +import com.group14.regicidechess.model.Lobby; +import com.group14.regicidechess.model.Move; + +public interface FirebaseAPI { + + // ── Lobby ───────────────────────────────────────────────────────────────── + + void createLobby(Lobby lobby, Callback onSuccess, Callback onError); + void joinLobby(String gameId, Callback onSuccess, Callback onError); + + /** Reads lobby settings without any side effects. Used by MainMenuScreen to validate. */ + void fetchLobby(String gameId, Callback onSuccess, Callback onError); + + /** + * Fires once when lobbies/{gameId}/status == "joined". + * HOST calls this to know a second player has entered the lobby. + */ + void listenForOpponentReady(String gameId, Runnable onReady); + + /** + * Sets lobbies/{gameId}/status = "started". + * HOST calls this when pressing "Start Game" so the joiner navigates to SetupScreen. + */ + void startGame(String gameId); + + /** + * Fires once when lobbies/{gameId}/status == "started". + * JOINER calls this to know the host has pressed "Start Game". + */ + void listenForGameStart(String gameId, Runnable onStart); + + // ── Setup ───────────────────────────────────────────────────────────────── + + /** + * Saves this player's board layout and marks them as setup-ready. + * Sets games/{gameId}/bothReady = true once BOTH players have called this. + */ + void confirmSetup(String gameId, boolean isWhite, int[][] board, Runnable onSuccess); + + /** Fetches the opponent's stored board layout. */ + void getOpponentBoard(String gameId, boolean localIsWhite, Callback onBoard); + + /** + * Fires once when games/{gameId}/bothReady == true. + * BOTH players call this in SetupScreen after confirming their boards. + * NOTE: Completely separate from listenForGameStart() which is lobby-phase only. + */ + void listenForBothSetupReady(String gameId, Runnable onBothReady); + + // ── In-match ────────────────────────────────────────────────────────────── + + void saveMove(String gameId, Move move, Runnable onSuccess); + + /** + * Fires for every move added to games/{gameId}/moves. + * Returns int[]{fromCol, fromRow, toCol, toRow, isWhite(1=white/0=black)}. + * Caller filters out own moves by comparing isWhite to localPlayer.isWhite(). + */ + void listenForOpponentMove(String gameId, Callback onMove); + + void sendHeartbeat(String gameId, boolean isWhite); + + /** + * Listens for the opponent's heartbeat. + * @param onHeartbeat called with the latency (delay) in ms when a heartbeat arrives. + * @param onTimeout called if no heartbeat is received within timeoutMs. + */ + void listenForHeartbeat(String gameId, boolean listenForWhite, long timeoutMs, + Callback onHeartbeat, Runnable onTimeout); + + /** e.g. "forfeit:white", "forfeit:black" */ + void signalGameOver(String gameId, String reason); + void listenForGameOver(String gameId, Callback onGameOver); + + // ───────────────────────────────────────────────────────────────────────── + + interface Callback { + void call(T value); + } +} diff --git a/core/src/main/java/com/group14/regicidechess/model/Board.java b/core/src/main/java/com/group14/regicidechess/model/Board.java index 06bb92c..67e2cc8 100644 --- a/core/src/main/java/com/group14/regicidechess/model/Board.java +++ b/core/src/main/java/com/group14/regicidechess/model/Board.java @@ -71,6 +71,10 @@ public ChessPiece movePiece(int fromCol, int fromRow, int toCol, int toRow) { ChessPiece moving = removePiece(fromCol, fromRow); ChessPiece captured = pieces[toCol][toRow]; // may be null placePiece(moving, toCol, toRow); + // Notify pawn it has moved so the two-square rule is disabled next turn + if (moving instanceof com.group14.regicidechess.model.pieces.Pawn) { + ((com.group14.regicidechess.model.pieces.Pawn) moving).markMoved(); + } return captured; } diff --git a/core/src/main/java/com/group14/regicidechess/model/Move.java b/core/src/main/java/com/group14/regicidechess/model/Move.java index 5dc919c..1c6522b 100644 --- a/core/src/main/java/com/group14/regicidechess/model/Move.java +++ b/core/src/main/java/com/group14/regicidechess/model/Move.java @@ -10,6 +10,7 @@ public class Move { private final Vector2 to; private final ChessPiece piece; private final Player player; + private String promotion; // e.g. "Queen", "Rook", etc. public Move(Vector2 from, Vector2 to, ChessPiece piece, Player player) { this.from = from.cpy(); @@ -18,8 +19,15 @@ public Move(Vector2 from, Vector2 to, ChessPiece piece, Player player) { this.player = player; } - public Vector2 getFrom() { return from.cpy(); } - public Vector2 getTo() { return to.cpy(); } - public ChessPiece getPiece() { return piece; } - public Player getPlayer() { return player; } + public Move(Vector2 from, Vector2 to, ChessPiece piece, Player player, String promotion) { + this(from, to, piece, player); + this.promotion = promotion; + } + + public Vector2 getFrom() { return from.cpy(); } + public Vector2 getTo() { return to.cpy(); } + public ChessPiece getPiece() { return piece; } + public Player getPlayer() { return player; } + public String getPromotion() { return promotion; } + public void setPromotion(String promotion) { this.promotion = promotion; } } diff --git a/core/src/main/java/com/group14/regicidechess/model/pieces/Pawn.java b/core/src/main/java/com/group14/regicidechess/model/pieces/Pawn.java index 207ee3e..cab9443 100644 --- a/core/src/main/java/com/group14/regicidechess/model/pieces/Pawn.java +++ b/core/src/main/java/com/group14/regicidechess/model/pieces/Pawn.java @@ -6,9 +6,10 @@ import java.util.ArrayList; import java.util.List; -/** Placement: core/src/main/java/com/group14/regicidechess/model/pieces/Pawn.java */ public class Pawn extends ChessPiece { + private boolean hasMoved = false; + public Pawn(Player owner) { super(owner, 1); } @@ -16,14 +17,19 @@ public Pawn(Player owner) { @Override public List validMoves() { List moves = new ArrayList<>(); - // White moves up (+row), black moves down (-row) int dir = owner.isWhite() ? 1 : -1; int c = (int) position.x; int r = (int) position.y; - // Forward — only if square is empty (FR1.1) + // One square forward if (board.inBounds(c, r + dir) && board.getPieceAt(c, r + dir) == null) { moves.add(new Vector2(c, r + dir)); + + // Two squares forward on first move (FR1.2) + if (!hasMoved && board.inBounds(c, r + 2 * dir) + && board.getPieceAt(c, r + 2 * dir) == null) { + moves.add(new Vector2(c, r + 2 * dir)); + } } // Diagonal captures @@ -36,9 +42,15 @@ public List validMoves() { } } } - // No en passant (§2.1.3) return moves; } + /** Called by Board.movePiece() after the pawn is moved. */ + public void markMoved() { + hasMoved = true; + } + + public boolean hasMoved() { return hasMoved; } + @Override public String getTypeName() { return "Pawn"; } } \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/GameScreen.java b/core/src/main/java/com/group14/regicidechess/screens/GameScreen.java deleted file mode 100644 index 70866fe..0000000 --- a/core/src/main/java/com/group14/regicidechess/screens/GameScreen.java +++ /dev/null @@ -1,468 +0,0 @@ -package com.group14.regicidechess.screens; - -import com.badlogic.gdx.Game; -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.Screen; -import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.graphics.GL20; -import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.g2d.SpriteBatch; -import com.badlogic.gdx.graphics.glutils.ShapeRenderer; -import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.scenes.scene2d.Actor; -import com.badlogic.gdx.scenes.scene2d.Stage; -import com.badlogic.gdx.scenes.scene2d.ui.Label; -import com.badlogic.gdx.scenes.scene2d.ui.Skin; -import com.badlogic.gdx.scenes.scene2d.ui.Table; -import com.badlogic.gdx.scenes.scene2d.ui.TextButton; -import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; -import com.badlogic.gdx.utils.Align; -import com.badlogic.gdx.utils.viewport.FitViewport; -import com.group14.regicidechess.input.ScreenInputHandler; -import com.group14.regicidechess.model.Board; -import com.group14.regicidechess.model.Player; -import com.group14.regicidechess.model.pieces.ChessPiece; -import com.group14.regicidechess.model.pieces.King; -import com.group14.regicidechess.states.InMatchState; -import com.group14.regicidechess.utils.ResourceManager; - -import java.util.ArrayList; -import java.util.List; - -public class GameScreen implements Screen, ScreenInputHandler.ScreenInputObserver { - - // ── Layout constants ────────────────────────────────────────────────────── - private static final float V_WIDTH = 480f; - private static final float V_HEIGHT = 854f; - private static final float TOP_BAR_HEIGHT = 70f; - private static final float STATUS_BAR_HEIGHT = 60f; - - // ── LibGDX / GUI fields ─────────────────────────────────────────────────── - private final Game game; - private final SpriteBatch batch; - private final Stage stage; - private final Skin skin; - private final ScreenInputHandler inputHandler; - private final ShapeRenderer shapeRenderer; - - // ── Game Logic layer references ─────────────────────────────────────────── - private final InMatchState inMatchState; - private final Player localPlayer; - - // ── Selection FSM ───────────────────────────────────────────────────────── - private Vector2 selectedCell = null; - private List validMoves = new ArrayList<>(); - - // ── Board geometry ──────────────────────────────────────────────────────── - private float boardLeft; - private float boardBottom; - private float cellSize; - - // ── Scene2D widgets ─────────────────────────────────────────────────────── - private Label turnLabel; - private Label statusLabel; - - // ── Overlay (forfeit confirm / game over) — built once, shown/hidden ────── - private Table overlayTable; - private Table overlayWrapper; // wraps dimmer + card, hidden by default - private Label overlayTitle; - private Label overlayBody; - private TextButton overlayConfirmBtn; - private TextButton overlayCancelBtn; - - // What the confirm button should do — set before showing overlay - private Runnable onOverlayConfirm; - - // ───────────────────────────────────────────────────────────────────────── - // Constructor - // ───────────────────────────────────────────────────────────────────────── - - public GameScreen(Game game, SpriteBatch batch, - Board board, Player localPlayer, int boardSize) { - this.game = game; - this.batch = batch; - this.localPlayer = localPlayer; - - Player opponent = new Player( - localPlayer.isWhite() ? "player2" : "player1", - !localPlayer.isWhite(), - localPlayer.getBudget()); - - inMatchState = new InMatchState(); - inMatchState.init(board, - localPlayer.isWhite() ? localPlayer : opponent, - localPlayer.isWhite() ? opponent : localPlayer); - inMatchState.enter(); - - stage = new Stage(new FitViewport(V_WIDTH, V_HEIGHT), batch); - skin = ResourceManager.getInstance().getSkin(); - shapeRenderer = new ShapeRenderer(); - inputHandler = new ScreenInputHandler(); - inputHandler.addObserver(this); - - buildUI(); - computeBoardGeometry(V_WIDTH, V_HEIGHT, boardSize); - refreshTurnLabel(); - } - - // ───────────────────────────────────────────────────────────────────────── - // UI construction - // ───────────────────────────────────────────────────────────────────────── - - private void buildUI() { - Table root = new Table(); - root.setFillParent(true); - root.top(); - stage.addActor(root); - - // ── Top bar ─────────────────────────────────────────────────────────── - Table topBar = new Table(); - topBar.setBackground(skin.getDrawable("primary-pixel")); - topBar.pad(10); - - TextButton forfeitBtn = new TextButton("Forfeit", skin, "danger"); - forfeitBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - showForfeitOverlay(); - } - }); - - turnLabel = new Label("", skin, "title"); - turnLabel.setAlignment(Align.center); - - topBar.add(forfeitBtn).width(120).height(50).left(); - topBar.add(turnLabel).expandX().center(); - topBar.add().width(120); - - root.add(topBar).expandX().fillX().height(TOP_BAR_HEIGHT).row(); - root.add().expandX().expandY().row(); - - // ── Status bar ──────────────────────────────────────────────────────── - Table statusBar = new Table(); - statusBar.setBackground(skin.getDrawable("surface-pixel")); - statusBar.pad(10); - - statusLabel = new Label("Select a piece to move.", skin, "small"); - statusLabel.setAlignment(Align.center); - statusBar.add(statusLabel).expandX(); - - root.add(statusBar).expandX().fillX().height(STATUS_BAR_HEIGHT).row(); - - // ── Overlay (shared by forfeit confirm + game over) ─────────────────── - buildOverlay(); - } - - private void buildOverlay() { - // overlayWrapper covers the full screen — centres the card. - // No background here; dimmer is drawn via ShapeRenderer in render(). - overlayWrapper = new Table(); - overlayWrapper.setFillParent(true); - overlayWrapper.setVisible(false); - stage.addActor(overlayWrapper); - - // Centred card inside the wrapper - overlayTable = new Table(); - overlayTable.setBackground(skin.getDrawable("surface-pixel")); - overlayTable.pad(32); - - overlayTitle = new Label("", skin, "title"); - overlayTitle.setAlignment(Align.center); - - overlayBody = new Label("", skin, "default"); - overlayBody.setAlignment(Align.center); - overlayBody.setWrap(true); - - overlayConfirmBtn = new TextButton("", skin, "accent"); - overlayCancelBtn = new TextButton("Cancel", skin, "default"); - - overlayTable.add(overlayTitle).expandX().padBottom(16).row(); - overlayTable.add(overlayBody).width(300).padBottom(32).row(); - - Table btnRow = new Table(); - btnRow.add(overlayCancelBtn).width(130).height(55).padRight(12); - btnRow.add(overlayConfirmBtn).width(130).height(55); - overlayTable.add(btnRow).row(); - - // Centre the card inside the full-screen wrapper - overlayWrapper.add(overlayTable).center(); - - // Wire listeners once — behaviour set via onOverlayConfirm - overlayConfirmBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - overlayWrapper.setVisible(false); - if (onOverlayConfirm != null) onOverlayConfirm.run(); - } - }); - overlayCancelBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - overlayWrapper.setVisible(false); - } - }); - } - - // ───────────────────────────────────────────────────────────────────────── - // Overlay helpers - // ───────────────────────────────────────────────────────────────────────── - - private void showForfeitOverlay() { - overlayTitle.setText("Forfeit?"); - overlayBody.setText("Are you sure you want to give up?"); - overlayConfirmBtn.setText("Yes, forfeit"); - overlayCancelBtn.setVisible(true); - onOverlayConfirm = () -> showGameOverOverlay(false); - overlayWrapper.setVisible(true); - } - - private void showGameOverOverlay(boolean localWon) { - overlayTitle.setText(localWon ? "You Win!" : "Game Over"); - overlayBody.setText(localWon - ? "You captured the opponent's King!" - : "Your King was captured."); - overlayConfirmBtn.setText("Back to Menu"); - overlayCancelBtn.setVisible(false); - onOverlayConfirm = () -> game.setScreen(new MainMenuScreen(game, batch)); - overlayWrapper.setVisible(true); - } - - // ───────────────────────────────────────────────────────────────────────── - // Board geometry - // ───────────────────────────────────────────────────────────────────────── - - private void computeBoardGeometry(float screenW, float screenH, int size) { - float available = screenH - TOP_BAR_HEIGHT - STATUS_BAR_HEIGHT - 16f; - float maxW = screenW - 16f; - cellSize = Math.min(maxW / size, available / size); - boardLeft = (screenW - cellSize * size) / 2f; - boardBottom = STATUS_BAR_HEIGHT + (available - cellSize * size) / 2f + 8f; - } - - // ───────────────────────────────────────────────────────────────────────── - // Rendering - // ───────────────────────────────────────────────────────────────────────── - - @Override - public void render(float delta) { - Gdx.gl.glClearColor(0.12f, 0.12f, 0.15f, 1f); - Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); - inMatchState.update(delta); - drawBoard(); - stage.act(delta); - stage.draw(); - - // Draw semi-transparent dimmer on top of board but below the overlay card - if (overlayWrapper.isVisible()) { - Gdx.gl.glEnable(GL20.GL_BLEND); - Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); - shapeRenderer.setProjectionMatrix(stage.getCamera().combined); - shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); - shapeRenderer.setColor(0f, 0f, 0f, 0.65f); - shapeRenderer.rect(0, 0, V_WIDTH, V_HEIGHT); - shapeRenderer.end(); - Gdx.gl.glDisable(GL20.GL_BLEND); - // Redraw stage so the overlay card appears on top of the dimmer - stage.draw(); - } - } - - private void drawBoard() { - Board board = inMatchState.getBoard(); - int size = board.getSize(); - - shapeRenderer.setProjectionMatrix(stage.getCamera().combined); - - // ── Tiles ───────────────────────────────────────────────────────────── - shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); - for (int col = 0; col < size; col++) { - for (int row = 0; row < size; row++) { - float x = boardLeft + col * cellSize; - float y = boardBottom + row * cellSize; - boolean light = (col + row) % 2 == 0; - shapeRenderer.setColor(light - ? new Color(0.93f, 0.85f, 0.72f, 1f) - : new Color(0.55f, 0.38f, 0.24f, 1f)); - shapeRenderer.rect(x, y, cellSize, cellSize); - } - } - shapeRenderer.end(); - - // ── Valid-move highlights ───────────────────────────────────────────── - if (!validMoves.isEmpty()) { - shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); - shapeRenderer.setColor(new Color(0.35f, 0.75f, 0.35f, 0.55f)); - for (Vector2 m : validMoves) - shapeRenderer.rect(boardLeft + m.x * cellSize, - boardBottom + m.y * cellSize, cellSize, cellSize); - shapeRenderer.end(); - } - - // ── Selected cell highlight ─────────────────────────────────────────── - if (selectedCell != null) { - shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); - shapeRenderer.setColor(new Color(0.85f, 0.65f, 0.13f, 0.60f)); - shapeRenderer.rect(boardLeft + selectedCell.x * cellSize, - boardBottom + selectedCell.y * cellSize, cellSize, cellSize); - shapeRenderer.end(); - } - - // ── Grid lines ──────────────────────────────────────────────────────── - shapeRenderer.begin(ShapeRenderer.ShapeType.Line); - shapeRenderer.setColor(new Color(0f, 0f, 0f, 0.25f)); - for (int i = 0; i <= size; i++) { - float x = boardLeft + i * cellSize; - float y = boardBottom + i * cellSize; - shapeRenderer.line(x, boardBottom, x, boardBottom + size * cellSize); - shapeRenderer.line(boardLeft, y, boardLeft + size * cellSize, y); - } - shapeRenderer.end(); - - // ── Pieces ──────────────────────────────────────────────────────────── - batch.setProjectionMatrix(stage.getCamera().combined); - batch.begin(); - - for (ChessPiece piece : board.getPieces()) { - Vector2 pos = piece.getPosition(); - String color = piece.getOwner().isWhite() ? "white" : "black"; - String type = piece.getTypeName().toLowerCase(); - Texture tex = ResourceManager.getInstance().getPieceTexture(color, type); - - float pieceSize = cellSize * 0.8f; - float offset = (cellSize - pieceSize) / 2f; - float px = boardLeft + pos.x * cellSize + offset; - float py = boardBottom + pos.y * cellSize + offset; - - batch.draw(tex, px, py, pieceSize, pieceSize); - } - - batch.end(); - } - - // ───────────────────────────────────────────────────────────────────────── - // Input — ScreenInputObserver - // ───────────────────────────────────────────────────────────────────────── - - @Override - public void onTap(int screenX, int screenY, int pointer, int button) { - // Ignore board taps when an overlay is visible - if (overlayWrapper.isVisible()) return; - Vector2 world = stage.getViewport().unproject(new Vector2(screenX, screenY)); - handleBoardTap(world.x, world.y); - } - - @Override public void onDrag (int x, int y, int pointer) {} - @Override public void onRelease(int x, int y, int pointer, int button) {} - @Override public void onKeyDown(int keycode) {} - - private void handleBoardTap(float worldX, float worldY) { - if (!inMatchState.isMyTurn(localPlayer)) { - showStatus("Not your turn."); - return; - } - - Board board = inMatchState.getBoard(); - int size = board.getSize(); - - if (worldX < boardLeft || worldX > boardLeft + size * cellSize) return; - if (worldY < boardBottom || worldY > boardBottom + size * cellSize) return; - - int col = Math.max(0, Math.min((int)((worldX - boardLeft) / cellSize), size - 1)); - int row = Math.max(0, Math.min((int)((worldY - boardBottom) / cellSize), size - 1)); - - Vector2 tapped = new Vector2(col, row); - - if (selectedCell == null) { - trySelect(tapped); - } else { - if (containsVector(validMoves, tapped)) { - executeMove(selectedCell, tapped); - } else { - ChessPiece occupant = board.getPieceAt(col, row); - if (occupant != null && occupant.getOwner() == localPlayer) { - trySelect(tapped); - } else { - deselect(); - } - } - } - } - - private void trySelect(Vector2 cell) { - Board board = inMatchState.getBoard(); - ChessPiece piece = board.getPieceAt((int) cell.x, (int) cell.y); - - if (piece == null || piece.getOwner() != localPlayer) { - showStatus("Select one of your own pieces."); - deselect(); - return; - } - selectedCell = cell; - validMoves = piece.validMoves(); - showStatus("Selected: " + piece.getTypeName() - + " — " + validMoves.size() + " move(s) available."); - } - - private void deselect() { - selectedCell = null; - validMoves.clear(); - showStatus("Select a piece to move."); - } - - private void executeMove(Vector2 from, Vector2 to) { - ChessPiece captured = inMatchState.executeMove(from, to); - deselect(); - refreshTurnLabel(); - - // TODO: DatabaseManager.getInstance().saveMove(...) - - if (captured instanceof King) { - showGameOverOverlay(true); - } else { - showStatus(inMatchState.isMyTurn(localPlayer) - ? "Your turn!" - : "Waiting for opponent..."); - } - } - - // ───────────────────────────────────────────────────────────────────────── - // Helpers - // ───────────────────────────────────────────────────────────────────────── - - private void refreshTurnLabel() { - Player current = inMatchState.getCurrentTurn(); - turnLabel.setText("Turn: " + (current.isWhite() ? "White" : "Black")); - } - - private void showStatus(String msg) { statusLabel.setText(msg); } - - private boolean containsVector(List list, Vector2 v) { - for (Vector2 item : list) - if ((int) item.x == (int) v.x && (int) item.y == (int) v.y) return true; - return false; - } - - // ───────────────────────────────────────────────────────────────────────── - // Screen lifecycle - // ───────────────────────────────────────────────────────────────────────── - - @Override - public void show() { - com.badlogic.gdx.InputMultiplexer mx = - new com.badlogic.gdx.InputMultiplexer(stage, inputHandler); - Gdx.input.setInputProcessor(mx); - } - - @Override - public void resize(int width, int height) { - stage.getViewport().update(width, height, true); - computeBoardGeometry(V_WIDTH, V_HEIGHT, inMatchState.getBoard().getSize()); - } - - @Override public void pause() {} - @Override public void resume() {} - @Override public void hide() { inputHandler.clearObservers(); inMatchState.exit(); } - - @Override - public void dispose() { - stage.dispose(); - shapeRenderer.dispose(); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/LobbyScreen.java b/core/src/main/java/com/group14/regicidechess/screens/LobbyScreen.java deleted file mode 100644 index 87dd412..0000000 --- a/core/src/main/java/com/group14/regicidechess/screens/LobbyScreen.java +++ /dev/null @@ -1,310 +0,0 @@ -package com.group14.regicidechess.screens; - -import com.badlogic.gdx.Game; -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.Screen; -import com.badlogic.gdx.graphics.GL20; -import com.badlogic.gdx.graphics.g2d.SpriteBatch; -import com.badlogic.gdx.scenes.scene2d.Actor; -import com.badlogic.gdx.scenes.scene2d.Stage; -import com.badlogic.gdx.scenes.scene2d.ui.Label; -import com.badlogic.gdx.scenes.scene2d.ui.Skin; -import com.badlogic.gdx.scenes.scene2d.ui.Slider; -import com.badlogic.gdx.scenes.scene2d.ui.Table; -import com.badlogic.gdx.scenes.scene2d.ui.TextButton; -import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; -import com.badlogic.gdx.utils.Align; -import com.badlogic.gdx.utils.viewport.FitViewport; -import com.group14.regicidechess.input.ScreenInputHandler; -import com.group14.regicidechess.states.LobbyState; -import com.group14.regicidechess.utils.ResourceManager; - -/** - * LobbyScreen — GUI layer for lobby creation and joining. - * - * Placement: core/src/main/java/com/group14/regicidechess/screens/LobbyScreen.java - * - * Owns: rendering, sliders, button listeners. - * Delegates: Game ID generation, player creation, lobby config → LobbyState. - */ -public class LobbyScreen implements Screen, ScreenInputHandler.ScreenInputObserver { - - public enum Mode { HOST, JOIN } - - private static final float V_WIDTH = 480f; - private static final float V_HEIGHT = 854f; - - private static final int BOARD_MIN = 5; - private static final int BOARD_MAX = 8; - private static final int BOARD_DEFAULT = 8; - - private static final int BUDGET_MIN = 5; - private static final int BUDGET_MAX = 50; - private static final int BUDGET_DEFAULT = 25; - private static final int BUDGET_STEP = 1; - - // ── LibGDX / GUI fields ─────────────────────────────────────────────────── - private final Game game; - private final SpriteBatch batch; - private final Mode mode; - private final String incomingGameId; // non-null in JOIN mode - - private final Stage stage; - private final Skin skin; - private final ScreenInputHandler inputHandler; - - // ── Game Logic layer reference ──────────────────────────────────────────── - private final LobbyState lobbyState; - - // ── Slider values (HOST mode — read into LobbyState on confirm) ────────── - private int boardSize = BOARD_DEFAULT; - private int budget = BUDGET_DEFAULT; - - // ── Scene2D labels that update at runtime ───────────────────────────────── - private Label boardSizeValueLabel; - private Label budgetValueLabel; - private Label statusLabel; - - // ───────────────────────────────────────────────────────────────────────── - // Constructor - // ───────────────────────────────────────────────────────────────────────── - - public LobbyScreen(Game game, SpriteBatch batch, Mode mode, String gameId) { - this.game = game; - this.batch = batch; - this.mode = mode; - this.incomingGameId = gameId; - - // Initialise Game Logic state - lobbyState = new LobbyState(); - lobbyState.enter(); - - stage = new Stage(new FitViewport(V_WIDTH, V_HEIGHT), batch); - skin = ResourceManager.getInstance().getSkin(); - - inputHandler = new ScreenInputHandler(); - inputHandler.addObserver(this); - - buildUI(); - } - - // ───────────────────────────────────────────────────────────────────────── - // UI construction - // ───────────────────────────────────────────────────────────────────────── - - private void buildUI() { - Table root = new Table(); - root.setFillParent(true); - root.setBackground(skin.getDrawable("surface-pixel")); - root.top().pad(32); - stage.addActor(root); - - String titleText = (mode == Mode.HOST) ? "Create Lobby" : "Join Lobby"; - Label titleLabel = new Label(titleText, skin, "title"); - titleLabel.setAlignment(Align.center); - root.add(titleLabel).expandX().padBottom(32).row(); - - if (mode == Mode.HOST) { - buildHostUI(root); - } else { - buildJoinUI(root); - } - - TextButton backBtn = new TextButton("Back", skin, "default"); - backBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - game.setScreen(new MainMenuScreen(game, batch)); - } - }); - root.add(backBtn).width(200).height(50).padTop(24).row(); - } - - // ── Host UI ─────────────────────────────────────────────────────────────── - - private void buildHostUI(Table root) { - // Board size slider - Label boardSizeLabel = new Label("Board Size", skin); - boardSizeValueLabel = new Label(boardSize + " x " + boardSize, skin); - - Slider boardSlider = new Slider(BOARD_MIN, BOARD_MAX, 1, false, buildSliderStyle()); - boardSlider.setValue(BOARD_DEFAULT); - boardSlider.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - boardSize = (int) boardSlider.getValue(); - boardSizeValueLabel.setText(boardSize + " x " + boardSize); - } - }); - - // Budget slider - Label budgetLabel = new Label("Starting Budget", skin); - budgetValueLabel = new Label(String.valueOf(budget), skin); - - Slider budgetSlider = new Slider(BUDGET_MIN, BUDGET_MAX, BUDGET_STEP, false, buildSliderStyle()); - budgetSlider.setValue(BUDGET_DEFAULT); - budgetSlider.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - budget = (int) budgetSlider.getValue(); - budgetValueLabel.setText(String.valueOf(budget)); - } - }); - - // Confirm — delegates to LobbyState, then navigates - TextButton confirmBtn = new TextButton("Start Game", skin, "accent"); - confirmBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - onHostConfirm(); - } - }); - - root.add(buildRow(boardSizeLabel, boardSizeValueLabel)).expandX().fillX().padBottom(8).row(); - root.add(boardSlider).width(380).height(40).padBottom(24).row(); - root.add(buildRow(budgetLabel, budgetValueLabel)).expandX().fillX().padBottom(8).row(); - root.add(budgetSlider).width(380).height(40).padBottom(32).row(); - root.add(confirmBtn).width(280).height(60).padBottom(24).row(); - } - - // ── Join UI ─────────────────────────────────────────────────────────────── - - private void buildJoinUI(Table root) { - Label enteredLabel = new Label("Game ID: " + incomingGameId, skin, "title"); - enteredLabel.setAlignment(Align.center); - root.add(enteredLabel).expandX().padBottom(24).row(); - - boardSizeValueLabel = new Label("Board: fetching...", skin, "small"); - budgetValueLabel = new Label("Budget: fetching...", skin, "small"); - root.add(boardSizeValueLabel).expandX().padBottom(4).row(); - root.add(budgetValueLabel).expandX().padBottom(32).row(); - - statusLabel = new Label("", skin, "small"); - statusLabel.setAlignment(Align.center); - - TextButton joinBtn = new TextButton("Join Match", skin, "accent"); - joinBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - onJoinConfirm(); - } - }); - - root.add(joinBtn).width(280).height(60).padBottom(16).row(); - root.add(statusLabel).expandX().row(); - } - - // ───────────────────────────────────────────────────────────────────────── - // Actions — delegate to LobbyState - // ───────────────────────────────────────────────────────────────────────── - - /** - * HOST: asks LobbyState to generate the lobby (Game ID + Player One), - * then navigates to SetupScreen with the real lobby config. - */ - private void onHostConfirm() { - // LobbyState creates the Lobby and Player One - String generatedId = lobbyState.generateGameId(boardSize, budget); - Gdx.app.log("LobbyScreen", "Lobby created: " + generatedId - + " size=" + boardSize + " budget=" + budget); - - // TODO: DatabaseManager.getInstance().saveLobby(lobbyState.getLobby(), () -> { - // navigateToSetup(true); - // }); - - navigateToSetup(true); - } - - /** - * JOIN: asks LobbyState to fetch the lobby from the server, - * then navigates to SetupScreen as Player Two. - */ - private void onJoinConfirm() { - if (statusLabel != null) statusLabel.setText("Connecting..."); - - // LobbyState resolves lobby settings from Firebase (stub for now) - lobbyState.joinByGameId(incomingGameId); - - // TODO: wait for async fetch; for now navigate with stub defaults - // The real flow: lobbyState.joinByGameId fetches from DatabaseManager, - // updates lobbyState.getLobby(), then we call navigateToSetup(false). - navigateToSetup(false); - } - - /** - * Navigates to SetupScreen, reading board config from LobbyState.getLobby(). - * Falls back to slider values if the lobby isn't populated yet (stub path). - */ - private void navigateToSetup(boolean isHost) { - int size; - int bud; - - if (lobbyState.getLobby() != null) { - // Game Logic layer has the authoritative values - size = lobbyState.getLobby().getBoardSize(); - bud = lobbyState.getLobby().getBudget(); - } else { - // Fallback for join stub (lobby not yet fetched) - size = BOARD_DEFAULT; - bud = BUDGET_DEFAULT; - } - - String gameId = lobbyState.getLobby() != null - ? lobbyState.getLobby().getGameId() - : (incomingGameId != null ? incomingGameId : "LOCAL"); - - game.setScreen(new SetupScreen(game, batch, gameId, size, bud, isHost)); - } - - // ───────────────────────────────────────────────────────────────────────── - // Helpers - // ───────────────────────────────────────────────────────────────────────── - - private Table buildRow(Label left, Label right) { - Table row = new Table(); - row.add(left).expandX().left(); - row.add(right).expandX().right(); - return row; - } - - private Slider.SliderStyle buildSliderStyle() { - Slider.SliderStyle style = new Slider.SliderStyle(); - style.background = skin.getDrawable("primary-pixel"); - style.knob = skin.getDrawable("accent-pixel"); - return style; - } - - // ───────────────────────────────────────────────────────────────────────── - // ScreenInputObserver — all input handled by Scene2D widgets on this screen - // ───────────────────────────────────────────────────────────────────────── - - @Override public void onTap (int x, int y, int pointer, int button) {} - @Override public void onDrag (int x, int y, int pointer) {} - @Override public void onRelease(int x, int y, int pointer, int button) {} - @Override public void onKeyDown(int keycode) {} - - // ───────────────────────────────────────────────────────────────────────── - // Screen lifecycle - // ───────────────────────────────────────────────────────────────────────── - - @Override - public void show() { - com.badlogic.gdx.InputMultiplexer mx = - new com.badlogic.gdx.InputMultiplexer(stage, inputHandler); - Gdx.input.setInputProcessor(mx); - } - - @Override - public void render(float delta) { - Gdx.gl.glClearColor(0.12f, 0.12f, 0.15f, 1f); - Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); - lobbyState.update(delta); - stage.act(delta); - stage.draw(); - } - - @Override public void resize(int width, int height) { stage.getViewport().update(width, height, true); } - @Override public void pause() {} - @Override public void resume() {} - @Override public void hide() { inputHandler.clearObservers(); lobbyState.exit(); } - - @Override - public void dispose() { - stage.dispose(); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/MainMenuScreen.java b/core/src/main/java/com/group14/regicidechess/screens/MainMenuScreen.java deleted file mode 100644 index bb0fa77..0000000 --- a/core/src/main/java/com/group14/regicidechess/screens/MainMenuScreen.java +++ /dev/null @@ -1,245 +0,0 @@ -package com.group14.regicidechess.screens; - -import com.badlogic.gdx.Game; -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.Screen; -import com.badlogic.gdx.graphics.GL20; -import com.badlogic.gdx.graphics.g2d.SpriteBatch; -import com.badlogic.gdx.scenes.scene2d.Actor; -import com.badlogic.gdx.scenes.scene2d.Stage; -import com.badlogic.gdx.scenes.scene2d.ui.Label; -import com.badlogic.gdx.scenes.scene2d.ui.Skin; -import com.badlogic.gdx.scenes.scene2d.ui.Table; -import com.badlogic.gdx.scenes.scene2d.ui.TextButton; -import com.badlogic.gdx.scenes.scene2d.ui.TextField; -import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; -import com.badlogic.gdx.utils.Align; -import com.badlogic.gdx.utils.viewport.FitViewport; -import com.group14.regicidechess.input.ScreenInputHandler; -import com.group14.regicidechess.states.MainMenuState; -import com.group14.regicidechess.utils.ResourceManager; - -/** - * MainMenuScreen — GUI layer for the main menu. - * - * Placement: core/src/main/java/com/group14/regicidechess/screens/MainMenuScreen.java - * - * Owns: rendering, button listeners, join-panel visibility. - * Delegates: player identity → MainMenuState (when Firebase auth is added). - * - * No game logic lives here — this screen only routes to LobbyScreen. - */ -public class MainMenuScreen implements Screen, ScreenInputHandler.ScreenInputObserver { - - private static final float V_WIDTH = 480f; - private static final float V_HEIGHT = 854f; - - // ── LibGDX / GUI fields ─────────────────────────────────────────────────── - private final Game game; - private final SpriteBatch batch; - private final Stage stage; - private final Skin skin; - private final ScreenInputHandler inputHandler; - - // ── Game Logic layer reference ──────────────────────────────────────────── - private final MainMenuState mainMenuState; - - // ── Join panel widgets ──────────────────────────────────────────────────── - private Table joinPanel; - private TextField gameIdField; - private Label errorLabel; - - // ───────────────────────────────────────────────────────────────────────── - // Constructor - // ───────────────────────────────────────────────────────────────────────── - - public MainMenuScreen(Game game, SpriteBatch batch) { - this.game = game; - this.batch = batch; - - mainMenuState = new MainMenuState(); - mainMenuState.enter(); - - stage = new Stage(new FitViewport(V_WIDTH, V_HEIGHT), batch); - skin = ResourceManager.getInstance().getSkin(); - - inputHandler = new ScreenInputHandler(); - inputHandler.addObserver(this); - - buildUI(); - } - - // ───────────────────────────────────────────────────────────────────────── - // UI construction - // ───────────────────────────────────────────────────────────────────────── - - private void buildUI() { - Table root = new Table(); - root.setFillParent(true); - root.setBackground(skin.getDrawable("surface-pixel")); - stage.addActor(root); - - // ── Title ───────────────────────────────────────────────────────────── - Label titleLabel = new Label("REGICIDE\nCHESS", skin, "title"); - titleLabel.setAlignment(Align.center); - - Label subtitleLabel = new Label("online strategy chess", skin, "small"); - subtitleLabel.setAlignment(Align.center); - - // ── Buttons ─────────────────────────────────────────────────────────── - TextButton createBtn = new TextButton("Create Lobby", skin, "accent"); - TextButton joinBtn = new TextButton("Join Lobby", skin, "default"); - createBtn.pad(12); - joinBtn.pad(12); - - // ── Join panel (hidden until "Join Lobby" is tapped) ────────────────── - joinPanel = buildJoinPanel(); - joinPanel.setVisible(false); - - // ── Error label (visible only on validation failure) ────────────────── - errorLabel = new Label("", skin, "small"); - errorLabel.setColor(com.badlogic.gdx.graphics.Color.RED); - errorLabel.setAlignment(Align.center); - - // ── Layout ──────────────────────────────────────────────────────────── - root.add(titleLabel).expandX().padTop(120).padBottom(8).row(); - root.add(subtitleLabel).expandX().padBottom(80).row(); - root.add(createBtn).width(300).height(60).padBottom(20).row(); - root.add(joinBtn).width(300).height(60).padBottom(16).row(); - root.add(errorLabel).expandX().padBottom(8).row(); - root.add(joinPanel).width(320).row(); - - // ── Listeners ───────────────────────────────────────────────────────── - createBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - onCreateLobby(); - } - }); - - joinBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - toggleJoinPanel(); - } - }); - } - - private Table buildJoinPanel() { - Table panel = new Table(); - panel.setBackground(skin.getDrawable("primary-pixel")); - panel.pad(16); - - Label hint = new Label("Enter Game ID", skin, "small"); - gameIdField = new TextField("", skin); - gameIdField.setMessageText("e.g. ABC123"); - gameIdField.setMaxLength(10); - - TextButton confirmBtn = new TextButton("Join", skin, "accent"); - TextButton backBtn = new TextButton("Back", skin, "default"); - - panel.add(hint).colspan(2).padBottom(8).row(); - panel.add(gameIdField).width(200).height(50).padRight(8); - panel.add(confirmBtn).width(80).height(50).row(); - panel.add(backBtn).colspan(2).width(290).height(48).padTop(8).row(); - - confirmBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - onJoinLobby(gameIdField.getText().trim()); - } - }); - - backBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - joinPanel.setVisible(false); - clearError(); - } - }); - - return panel; - } - - // ───────────────────────────────────────────────────────────────────────── - // Actions - // ───────────────────────────────────────────────────────────────────────── - - /** Navigate to LobbyScreen in HOST mode. */ - private void onCreateLobby() { - clearError(); - game.setScreen(new LobbyScreen(game, batch, LobbyScreen.Mode.HOST, null)); - } - - /** Toggle the Game-ID input panel. */ - private void toggleJoinPanel() { - clearError(); - joinPanel.setVisible(!joinPanel.isVisible()); - } - - /** - * Client-side sanity check (FR3): Game ID must not be empty. - * LobbyScreen will ask the server to validate and fetch the lobby settings. - */ - private void onJoinLobby(String gameId) { - if (gameId.isEmpty()) { - showError("Please enter a Game ID."); - return; - } - clearError(); - game.setScreen(new LobbyScreen(game, batch, LobbyScreen.Mode.JOIN, gameId)); - } - - // ───────────────────────────────────────────────────────────────────────── - // Error display (visible in UI — replaces Gdx.app.log stub) - // ───────────────────────────────────────────────────────────────────────── - - private void showError(String message) { - errorLabel.setText(message); - Gdx.app.log("MainMenuScreen", message); - } - - private void clearError() { - errorLabel.setText(""); - } - - // ───────────────────────────────────────────────────────────────────────── - // ScreenInputObserver — Scene2D handles all widget input on this screen - // ───────────────────────────────────────────────────────────────────────── - - @Override public void onTap (int x, int y, int pointer, int button) {} - @Override public void onDrag (int x, int y, int pointer) {} - @Override public void onRelease(int x, int y, int pointer, int button) {} - @Override public void onKeyDown(int keycode) {} - - // ───────────────────────────────────────────────────────────────────────── - // Screen lifecycle - // ───────────────────────────────────────────────────────────────────────── - - @Override - public void show() { - com.badlogic.gdx.InputMultiplexer mx = - new com.badlogic.gdx.InputMultiplexer(stage, inputHandler); - Gdx.input.setInputProcessor(mx); - } - - @Override - public void render(float delta) { - Gdx.gl.glClearColor(0.12f, 0.12f, 0.15f, 1f); - Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); - mainMenuState.update(delta); - stage.act(delta); - stage.draw(); - } - - @Override public void resize(int width, int height) { stage.getViewport().update(width, height, true); } - @Override public void pause() {} - @Override public void resume() {} - - @Override - public void hide() { - inputHandler.clearObservers(); - mainMenuState.exit(); - } - - @Override - public void dispose() { - stage.dispose(); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/SetupScreen.java b/core/src/main/java/com/group14/regicidechess/screens/SetupScreen.java deleted file mode 100644 index e759a24..0000000 --- a/core/src/main/java/com/group14/regicidechess/screens/SetupScreen.java +++ /dev/null @@ -1,462 +0,0 @@ -package com.group14.regicidechess.screens; - -import com.badlogic.gdx.Game; -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.Screen; -import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.graphics.GL20; -import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.g2d.SpriteBatch; -import com.badlogic.gdx.graphics.glutils.ShapeRenderer; -import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.scenes.scene2d.Actor; -import com.badlogic.gdx.scenes.scene2d.Stage; -import com.badlogic.gdx.scenes.scene2d.ui.Label; -import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; -import com.badlogic.gdx.scenes.scene2d.ui.Skin; -import com.badlogic.gdx.scenes.scene2d.ui.Table; -import com.badlogic.gdx.scenes.scene2d.ui.TextButton; -import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; -import com.badlogic.gdx.utils.Align; -import com.badlogic.gdx.utils.viewport.FitViewport; -import com.group14.regicidechess.input.ScreenInputHandler; -import com.group14.regicidechess.model.Player; -import com.group14.regicidechess.model.pieces.Bishop; -import com.group14.regicidechess.model.pieces.ChessPiece; -import com.group14.regicidechess.model.pieces.King; -import com.group14.regicidechess.model.pieces.Knight; -import com.group14.regicidechess.model.pieces.Pawn; -import com.group14.regicidechess.model.pieces.Queen; -import com.group14.regicidechess.model.pieces.Rook; -import com.group14.regicidechess.states.SetupState; -import com.group14.regicidechess.utils.ResourceManager; - -public class SetupScreen implements Screen, ScreenInputHandler.ScreenInputObserver { - - // ── Layout constants ────────────────────────────────────────────────────── - private static final float V_WIDTH = 480f; - private static final float V_HEIGHT = 854f; - private static final float HEADER_HEIGHT = 80f; - private static final float PALETTE_HEIGHT = 130f; - private static final float FOOTER_HEIGHT = 70f; - - // ── Piece display data ──────────────────────────────────────────────────── - private static final String[] PIECE_NAMES = { "King", "Queen", "Rook", "Bishop", "Knight", "Pawn" }; - private static final int[] PIECE_COSTS = { 0, 9, 5, 3, 3, 1 }; - - // ── LibGDX / GUI fields ─────────────────────────────────────────────────── - private final Game game; - private final SpriteBatch batch; - private final Stage stage; - private final Skin skin; - private final ScreenInputHandler inputHandler; - private final ShapeRenderer shapeRenderer; - - // ── Game Logic layer reference ──────────────────────────────────────────── - private final SetupState setupState; - private final Player localPlayer; - - // ── Palette selection (GUI-only state) ──────────────────────────────────── - private int selectedPieceIndex = -1; - private TextButton[] paletteButtons; - - // ── Board geometry ──────────────────────────────────────────────────────── - private float boardLeft; - private float boardBottom; - private float cellSize; - - // ── Scene2D widgets that update at runtime ──────────────────────────────── - private Label budgetLabel; - private Label statusLabel; - private TextButton confirmBtn; - - // ───────────────────────────────────────────────────────────────────────── - // Constructor - // ───────────────────────────────────────────────────────────────────────── - - public SetupScreen(Game game, SpriteBatch batch, - String gameId, int boardSize, int budget, boolean isHost) { - this.game = game; - this.batch = batch; - - localPlayer = new Player(isHost ? "player1" : "player2", isHost, budget); - - setupState = new SetupState(); - setupState.setBoardSize(boardSize); - setupState.setBudget(budget); - setupState.setPlayer(localPlayer); - setupState.enter(); - - stage = new Stage(new FitViewport(V_WIDTH, V_HEIGHT), batch); - skin = ResourceManager.getInstance().getSkin(); - shapeRenderer = new ShapeRenderer(); - inputHandler = new ScreenInputHandler(); - inputHandler.addObserver(this); - - buildUI(); - computeBoardGeometry(V_WIDTH, V_HEIGHT); - } - - // ───────────────────────────────────────────────────────────────────────── - // UI construction - // ───────────────────────────────────────────────────────────────────────── - - private void buildUI() { - Table root = new Table(); - root.setFillParent(true); - root.top(); - stage.addActor(root); - - // ── Header ──────────────────────────────────────────────────────────── - Table header = new Table(); - header.setBackground(skin.getDrawable("primary-pixel")); - header.pad(12); - - Label titleLabel = new Label("SETUP", skin, "title"); - budgetLabel = new Label(budgetText(), skin); - header.add(titleLabel).expandX().left(); - header.add(budgetLabel).expandX().right(); - root.add(header).expandX().fillX().height(HEADER_HEIGHT).row(); - - // ── Board spacer ────────────────────────────────────────────────────── - root.add().expandX().expandY().row(); - - // ── Piece palette ───────────────────────────────────────────────────── - Table paletteWrapper = new Table(); - paletteWrapper.setBackground(skin.getDrawable("primary-dark-pixel")); - paletteWrapper.pad(8); - - Table palette = new Table(); - paletteButtons = new TextButton[PIECE_NAMES.length]; - - for (int i = 0; i < PIECE_NAMES.length; i++) { - final int idx = i; - // Two blank lines at top — sprite will be drawn there by drawPaletteSprites() - String btnLabel = "\n\n" + PIECE_NAMES[i] - + "\n[" + (PIECE_COSTS[i] == 0 ? "free" : PIECE_COSTS[i]) + "]"; - TextButton btn = new TextButton(btnLabel, skin, "default"); - btn.getLabel().setAlignment(Align.center); - paletteButtons[i] = btn; - btn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - selectPalette(idx); - } - }); - palette.add(btn).width(68).height(90).pad(4); - } - - ScrollPane scroll = new ScrollPane(palette, skin); - scroll.setScrollingDisabled(false, true); - paletteWrapper.add(scroll).expandX().fillX().height(100); - root.add(paletteWrapper).expandX().fillX().height(PALETTE_HEIGHT).row(); - - // ── Footer ──────────────────────────────────────────────────────────── - Table footer = new Table(); - footer.setBackground(skin.getDrawable("surface-pixel")); - footer.pad(10); - - TextButton clearBtn = new TextButton("Clear", skin, "danger"); - confirmBtn = new TextButton("Confirm", skin, "accent"); - confirmBtn.setDisabled(true); - - statusLabel = new Label("Place your King to continue", skin, "small"); - statusLabel.setAlignment(Align.center); - - clearBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { onClear(); } - }); - confirmBtn.addListener(new ChangeListener() { - @Override public void changed(ChangeEvent event, Actor actor) { - if (!confirmBtn.isDisabled()) onConfirm(); - } - }); - - footer.add(clearBtn).width(140).height(50).expandX().left(); - footer.add(statusLabel).expandX(); - footer.add(confirmBtn).width(140).height(50).expandX().right(); - root.add(footer).expandX().fillX().height(FOOTER_HEIGHT).row(); - } - - // ───────────────────────────────────────────────────────────────────────── - // Board geometry - // ───────────────────────────────────────────────────────────────────────── - - private void computeBoardGeometry(float screenW, float screenH) { - float available = screenH - HEADER_HEIGHT - PALETTE_HEIGHT - FOOTER_HEIGHT - 16f; - float maxW = screenW - 16f; - int size = setupState.getBoardSize(); - - cellSize = Math.min(maxW / size, available / size); - boardLeft = (screenW - cellSize * size) / 2f; - boardBottom = FOOTER_HEIGHT + PALETTE_HEIGHT + (available - cellSize * size) / 2f + 8f; - } - - // ───────────────────────────────────────────────────────────────────────── - // Rendering - // ───────────────────────────────────────────────────────────────────────── - - @Override - public void render(float delta) { - Gdx.gl.glClearColor(0.12f, 0.12f, 0.15f, 1f); - Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); - setupState.update(delta); - drawBoard(); - stage.act(delta); - stage.draw(); - drawPaletteSprites(); // drawn after stage so sprites appear on top of buttons - } - - private void drawBoard() { - int size = setupState.getBoardSize(); - shapeRenderer.setProjectionMatrix(stage.getCamera().combined); - - // ── Tiles ───────────────────────────────────────────────────────────── - shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); - for (int col = 0; col < size; col++) { - for (int row = 0; row < size; row++) { - float x = boardLeft + col * cellSize; - float y = boardBottom + row * cellSize; - boolean light = (col + row) % 2 == 0; - boolean home = row >= setupState.getHomeRowMin() - && row <= setupState.getHomeRowMax(); - - if (home) { - shapeRenderer.setColor(light - ? new Color(0.93f, 0.90f, 0.75f, 1f) - : new Color(0.45f, 0.62f, 0.32f, 1f)); - } else { - shapeRenderer.setColor(light - ? new Color(0.93f, 0.85f, 0.72f, 1f) - : new Color(0.55f, 0.38f, 0.24f, 1f)); - } - shapeRenderer.rect(x, y, cellSize, cellSize); - } - } - shapeRenderer.end(); - - // ── Grid lines ──────────────────────────────────────────────────────── - shapeRenderer.begin(ShapeRenderer.ShapeType.Line); - shapeRenderer.setColor(new Color(0f, 0f, 0f, 0.3f)); - for (int i = 0; i <= size; i++) { - float x = boardLeft + i * cellSize; - float y = boardBottom + i * cellSize; - shapeRenderer.line(x, boardBottom, x, boardBottom + size * cellSize); - shapeRenderer.line(boardLeft, y, boardLeft + size * cellSize, y); - } - shapeRenderer.end(); - - // ── Home zone border ────────────────────────────────────────────────── - shapeRenderer.begin(ShapeRenderer.ShapeType.Line); - shapeRenderer.setColor(new Color(0.4f, 0.85f, 0.4f, 0.9f)); - Gdx.gl.glLineWidth(3f); - float zoneBottom = boardBottom + setupState.getHomeRowMin() * cellSize; - float zoneTop = boardBottom + (setupState.getHomeRowMax() + 1) * cellSize; - shapeRenderer.rect(boardLeft, zoneBottom, size * cellSize, zoneTop - zoneBottom); - Gdx.gl.glLineWidth(1f); - shapeRenderer.end(); - - // ── Placed pieces ───────────────────────────────────────────────────── - batch.setProjectionMatrix(stage.getCamera().combined); - batch.begin(); - - for (ChessPiece piece : setupState.getBoard().getPieces()) { - Vector2 pos = piece.getPosition(); - int col = (int) pos.x, row = (int) pos.y; - String color = localPlayer.isWhite() ? "white" : "black"; - String type = piece.getTypeName().toLowerCase(); - Texture tex = ResourceManager.getInstance().getPieceTexture(color, type); - - float pieceSize = cellSize * 0.8f; - float offset = (cellSize - pieceSize) / 2f; - float x = boardLeft + col * cellSize + offset; - float y = boardBottom + row * cellSize + offset; - - batch.draw(tex, x, y, pieceSize, pieceSize); - } - - batch.end(); - } - - /** - * Draws piece sprites on top of the palette buttons. - * Called after stage.draw() so sprites appear above the button backgrounds. - * Uses localToStageCoordinates() to correctly resolve nested actor positions - * (buttons live inside ScrollPane -> Table -> Table). - */ - private void drawPaletteSprites() { - String color = localPlayer.isWhite() ? "white" : "black"; - - batch.setProjectionMatrix(stage.getCamera().combined); - batch.begin(); - - for (int i = 0; i < paletteButtons.length; i++) { - TextButton btn = paletteButtons[i]; - String type = PIECE_NAMES[i].toLowerCase(); - Texture tex = ResourceManager.getInstance().getPieceTexture(color, type); - - float btnW = btn.getWidth(); - float btnH = btn.getHeight(); - - // Resolve button origin to stage coordinates - Vector2 stagePos = btn.localToStageCoordinates(new Vector2(0, 0)); - float btnX = stagePos.x; - float btnY = stagePos.y; - - // Sprite sits in the top portion of the button, above the text - float spriteSize = Math.min(btnW, btnH) * 0.42f; - float spriteX = btnX + (btnW - spriteSize) / 2f; - float spriteY = btnY + btnH - spriteSize - 4f; // 4px padding from top edge - - batch.draw(tex, spriteX, spriteY, spriteSize, spriteSize); - } - - batch.end(); - } - - // ───────────────────────────────────────────────────────────────────────── - // Input — ScreenInputObserver - // ───────────────────────────────────────────────────────────────────────── - - @Override - public void onTap(int screenX, int screenY, int pointer, int button) { - Vector2 world = stage.getViewport().unproject(new Vector2(screenX, screenY)); - handleBoardTap(world.x, world.y); - } - - @Override public void onDrag (int x, int y, int pointer) {} - @Override public void onRelease(int x, int y, int pointer, int button) {} - @Override public void onKeyDown(int keycode) {} - - private void handleBoardTap(float worldX, float worldY) { - int size = setupState.getBoardSize(); - if (worldX < boardLeft || worldX > boardLeft + size * cellSize) return; - if (worldY < boardBottom || worldY > boardBottom + size * cellSize) return; - - int col = Math.max(0, Math.min((int)((worldX - boardLeft) / cellSize), size - 1)); - int row = Math.max(0, Math.min((int)((worldY - boardBottom) / cellSize), size - 1)); - - ChessPiece existing = setupState.getBoard().getPieceAt(col, row); - - if (existing != null) { - setupState.removePiece(col, row); - } else if (selectedPieceIndex >= 0) { - ChessPiece piece = createPiece(selectedPieceIndex); - boolean ok = setupState.placePiece(piece, col, row); - if (!ok) { - // Give specific feedback for king limit vs other failures - if (piece instanceof King && kingIsOnBoard()) { - showStatus("You can only place one King!"); - } else { - showStatus("Cannot place here — check home zone and budget."); - } - } - } - - refreshUI(); - } - - private ChessPiece createPiece(int idx) { - switch (idx) { - case 0: return new King(localPlayer); - case 1: return new Queen(localPlayer); - case 2: return new Rook(localPlayer); - case 3: return new Bishop(localPlayer); - case 4: return new Knight(localPlayer); - case 5: return new Pawn(localPlayer); - default: throw new IllegalArgumentException("Unknown piece index: " + idx); - } - } - - // ───────────────────────────────────────────────────────────────────────── - // Palette - // ───────────────────────────────────────────────────────────────────────── - - private void selectPalette(int idx) { - selectedPieceIndex = (selectedPieceIndex == idx) ? -1 : idx; - for (int i = 0; i < paletteButtons.length; i++) { - paletteButtons[i].setStyle(skin.get( - i == selectedPieceIndex ? "accent" : "default", - TextButton.TextButtonStyle.class)); - } - } - - // ───────────────────────────────────────────────────────────────────────── - // Actions - // ───────────────────────────────────────────────────────────────────────── - - private void onClear() { - setupState.clearBoard(); - refreshUI(); - } - - private void onConfirm() { - boolean ready = setupState.setReady(); - if (!ready) { - showStatus("Place your King before confirming."); - return; - } - Gdx.app.log("SetupScreen", "Setup confirmed."); - // TODO: DatabaseManager.getInstance().confirmSetup(...) - navigateToGame(); - } - - private void navigateToGame() { - game.setScreen(new GameScreen(game, batch, - setupState.getBoard(), - localPlayer, - setupState.getBoardSize())); - } - - // ───────────────────────────────────────────────────────────────────────── - // UI refresh - // ───────────────────────────────────────────────────────────────────────── - - private void refreshUI() { - budgetLabel.setText(budgetText()); - confirmBtn.setDisabled(!kingIsOnBoard()); - if (!kingIsOnBoard()) { - showStatus("Place your King to continue."); - } else { - showStatus("Ready! Press Confirm when done."); - } - } - - private boolean kingIsOnBoard() { - for (ChessPiece p : setupState.getBoard().getPieces()) - if (p instanceof King) return true; - return false; - } - - private String budgetText() { - return "Budget: " + localPlayer.getBudgetRemaining() + " / " + localPlayer.getBudget(); - } - - private void showStatus(String msg) { statusLabel.setText(msg); } - - // ───────────────────────────────────────────────────────────────────────── - // Screen lifecycle - // ───────────────────────────────────────────────────────────────────────── - - @Override - public void show() { - com.badlogic.gdx.InputMultiplexer mx = - new com.badlogic.gdx.InputMultiplexer(stage, inputHandler); - Gdx.input.setInputProcessor(mx); - } - - @Override - public void resize(int width, int height) { - stage.getViewport().update(width, height, true); - computeBoardGeometry(V_WIDTH, V_HEIGHT); - } - - @Override public void pause() {} - @Override public void resume() {} - @Override public void hide() { inputHandler.clearObservers(); setupState.exit(); } - - @Override - public void dispose() { - stage.dispose(); - shapeRenderer.dispose(); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/game/GameBoardRenderer.java b/core/src/main/java/com/group14/regicidechess/screens/game/GameBoardRenderer.java new file mode 100644 index 0000000..a80347f --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/game/GameBoardRenderer.java @@ -0,0 +1,178 @@ +package com.group14.regicidechess.screens.game; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector2; +import com.group14.regicidechess.model.Board; +import com.group14.regicidechess.model.Player; +import com.group14.regicidechess.model.pieces.ChessPiece; +import com.group14.regicidechess.utils.ResourceManager; + +import java.util.List; + +/** + * GameBoardRenderer — draws the chess board, pieces, and selection highlights. + * + * Placement: core/src/main/java/com/group14/regicidechess/screens/game/GameBoardRenderer.java + * + * Owns the ShapeRenderer. Knows nothing about Firebase, screens, or game logic — + * it is given what to draw via method parameters and the shared board reference. + * + * Coordinate convention: + * Logic space — col/row as used by Board and InMatchState (row 0 = white's back rank). + * Display space — row after optional flip; row 0 is always at the bottom of the screen. + * White: display == logic. Black: display = (size - 1 - logic) so their pieces appear at the bottom. + */ +public class GameBoardRenderer { + + private final ShapeRenderer shapeRenderer; + private final SpriteBatch batch; + private final Player localPlayer; + private final int boardSize; + + private float boardLeft; + private float boardBottom; + private float cellSize; + + // Tile colours + private static final Color LIGHT_TILE = new Color(0.93f, 0.85f, 0.72f, 1f); + private static final Color DARK_TILE = new Color(0.55f, 0.38f, 0.24f, 1f); + + public GameBoardRenderer(SpriteBatch batch, Player localPlayer, int boardSize) { + this.batch = batch; + this.localPlayer = localPlayer; + this.boardSize = boardSize; + this.shapeRenderer = new ShapeRenderer(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Geometry + // ───────────────────────────────────────────────────────────────────────── + + public void computeGeometry(float screenW, float screenH, + float topBarHeight, float statusBarHeight) { + float available = screenH - topBarHeight - statusBarHeight - 16f; + float maxW = screenW - 16f; + cellSize = Math.min(maxW / boardSize, available / boardSize); + boardLeft = (screenW - cellSize * boardSize) / 2f; + boardBottom = statusBarHeight + (available - cellSize * boardSize) / 2f + 8f; + } + + /** Converts a logic row to a display row for this player's perspective. */ + public int toDisplayRow(int logicRow) { + return localPlayer.isWhite() ? logicRow : (boardSize - 1 - logicRow); + } + + /** Converts a display row back to a logic row. */ + public int toLogicRow(int displayRow) { + return localPlayer.isWhite() ? displayRow : (boardSize - 1 - displayRow); + } + + /** Returns the cell size in pixels (useful for tap detection in GameScreen). */ + public float getCellSize() { return cellSize; } + public float getBoardLeft() { return boardLeft; } + public float getBoardBottom(){ return boardBottom; } + + // ───────────────────────────────────────────────────────────────────────── + // Drawing + // ───────────────────────────────────────────────────────────────────────── + + /** + * Draws the full board: tiles, highlights, grid, and pieces. + * + * @param projMatrix camera combined matrix from the Stage + * @param board current game board + * @param selectedCell logic coords of the selected cell, or null + * @param validMoves logic coords of valid move targets, may be empty + */ + public void draw(Matrix4 projMatrix, Board board, + Vector2 selectedCell, List validMoves) { + shapeRenderer.setProjectionMatrix(projMatrix); + drawTiles(); + drawValidMoveHighlights(validMoves); + drawSelectedHighlight(selectedCell); + drawGrid(); + drawPieces(projMatrix, board); + } + + private void drawTiles() { + shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); + for (int col = 0; col < boardSize; col++) { + for (int logicRow = 0; logicRow < boardSize; logicRow++) { + int dispRow = toDisplayRow(logicRow); + float x = boardLeft + col * cellSize; + float y = boardBottom + dispRow * cellSize; + boolean light = (col + logicRow) % 2 == 0; + shapeRenderer.setColor(light ? LIGHT_TILE : DARK_TILE); + shapeRenderer.rect(x, y, cellSize, cellSize); + } + } + shapeRenderer.end(); + } + + private void drawValidMoveHighlights(List validMoves) { + if (validMoves == null || validMoves.isEmpty()) return; + shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); + shapeRenderer.setColor(new Color(0.35f, 0.75f, 0.35f, 0.55f)); + for (Vector2 m : validMoves) { + shapeRenderer.rect( + boardLeft + m.x * cellSize, + boardBottom + toDisplayRow((int) m.y) * cellSize, + cellSize, cellSize); + } + shapeRenderer.end(); + } + + private void drawSelectedHighlight(Vector2 selectedCell) { + if (selectedCell == null) return; + shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); + shapeRenderer.setColor(new Color(0.85f, 0.65f, 0.13f, 0.60f)); + shapeRenderer.rect( + boardLeft + selectedCell.x * cellSize, + boardBottom + toDisplayRow((int) selectedCell.y) * cellSize, + cellSize, cellSize); + shapeRenderer.end(); + } + + private void drawGrid() { + shapeRenderer.begin(ShapeRenderer.ShapeType.Line); + shapeRenderer.setColor(new Color(0f, 0f, 0f, 0.25f)); + for (int i = 0; i <= boardSize; i++) { + float x = boardLeft + i * cellSize; + float y = boardBottom + i * cellSize; + shapeRenderer.line(x, boardBottom, x, boardBottom + boardSize * cellSize); + shapeRenderer.line(boardLeft, y, boardLeft + boardSize * cellSize, y); + } + shapeRenderer.end(); + } + + private void drawPieces(Matrix4 projMatrix, Board board) { + batch.setProjectionMatrix(projMatrix); + batch.begin(); + for (ChessPiece piece : board.getPieces()) { + Vector2 pos = piece.getPosition(); + int dispRow = toDisplayRow((int) pos.y); + String color = piece.getOwner().isWhite() ? "white" : "black"; + Texture tex = ResourceManager.getInstance() + .getPieceTexture(color, piece.getTypeName().toLowerCase()); + float pieceSize = cellSize * 0.8f; + float offset = (cellSize - pieceSize) / 2f; + batch.draw(tex, + boardLeft + pos.x * cellSize + offset, + boardBottom + dispRow * cellSize + offset, + pieceSize, pieceSize); + } + batch.end(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Lifecycle + // ───────────────────────────────────────────────────────────────────────── + + public void dispose() { + shapeRenderer.dispose(); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/game/GameNetworkHandler.java b/core/src/main/java/com/group14/regicidechess/screens/game/GameNetworkHandler.java new file mode 100644 index 0000000..4a70618 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/game/GameNetworkHandler.java @@ -0,0 +1,151 @@ +package com.group14.regicidechess.screens.game; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.scenes.scene2d.ui.Image; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.group14.regicidechess.database.DatabaseManager; +import com.group14.regicidechess.model.Move; +import com.group14.regicidechess.model.Player; + +/** + * GameNetworkHandler — owns all Firebase subscriptions for an active game. + * + * Placement: core/src/main/java/com/group14/regicidechess/screens/game/GameNetworkHandler.java + * + * Receives raw Firebase data and forwards parsed, typed events to the Listener. + * Knows nothing about rendering or game logic — it only translates network events. + */ +public class GameNetworkHandler { + + /** Callbacks forwarded to GameScreen. All are already on the LibGDX GL thread. */ + public interface Listener { + /** + * Called when the opponent makes a move. + * @param from logic-space origin + * @param to logic-space destination + * @param promoCode 0 = no promotion; 2=Queen 3=Rook 4=Bishop 5=Knight + */ + void onOpponentMove(Vector2 from, Vector2 to, int promoCode); + + /** Called when the opponent forfeits or the game ends via signalGameOver. */ + void onGameOver(String reason); + + /** Called periodically with the measured latency to the opponent in ms. */ + void onHeartbeatLatency(long latencyMs); + + /** Called when no heartbeat has arrived within the timeout window. */ + void onOpponentDisconnected(); + } + + private static final long HEARTBEAT_TIMEOUT_MS = 15_000L; + + private final String gameId; + private final Player localPlayer; + private final Listener listener; + + // Connection UI refs — updated directly here to keep GameScreen clean + private final Image connectionIcon; + private final Label connectionLabel; + + public GameNetworkHandler(String gameId, Player localPlayer, Listener listener, + Image connectionIcon, Label connectionLabel) { + this.gameId = gameId; + this.localPlayer = localPlayer; + this.listener = listener; + this.connectionIcon = connectionIcon; + this.connectionLabel = connectionLabel; + } + + // ───────────────────────────────────────────────────────────────────────── + // Start all listeners + // ───────────────────────────────────────────────────────────────────────── + + public void start() { + startOpponentMoveListener(); + startGameOverListener(); + startHeartbeat(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Outgoing + // ───────────────────────────────────────────────────────────────────────── + + /** Saves a completed move (with optional promotion) to Firebase. */ + public void saveMove(Move move) { + DatabaseManager.getInstance().getApi().saveMove(gameId, move, () -> {}); + } + + /** Sends a heartbeat pulse. Call every HEARTBEAT_INTERVAL seconds from render(). */ + public void sendHeartbeat() { + DatabaseManager.getInstance().getApi().sendHeartbeat(gameId, localPlayer.isWhite()); + } + + /** Signals that this player has forfeited. */ + public void signalForfeit() { + String loser = localPlayer.isWhite() ? "white" : "black"; + DatabaseManager.getInstance().getApi().signalGameOver(gameId, "forfeit:" + loser); + } + + // ───────────────────────────────────────────────────────────────────────── + // Incoming listeners + // ───────────────────────────────────────────────────────────────────────── + + /** + * Listens for opponent moves. + * Firebase sends int[]{fromCol, fromRow, toCol, toRow, isWhite(1/0), promoCode}. + * Own echoed moves are filtered out via coords[4]. + */ + private void startOpponentMoveListener() { + DatabaseManager.getInstance().getApi() + .listenForOpponentMove(gameId, coords -> { + boolean moveIsWhite = coords.length > 4 && coords[4] == 1; + if (moveIsWhite == localPlayer.isWhite()) return; // own echo + + Vector2 from = new Vector2(coords[0], coords[1]); + Vector2 to = new Vector2(coords[2], coords[3]); + int promoCode = coords.length > 5 ? coords[5] : 0; + + Gdx.app.postRunnable(() -> listener.onOpponentMove(from, to, promoCode)); + }); + } + + private void startGameOverListener() { + DatabaseManager.getInstance().getApi() + .listenForGameOver(gameId, reason -> + Gdx.app.postRunnable(() -> listener.onGameOver(reason))); + } + + private void startHeartbeat() { + // Send our first beat immediately + DatabaseManager.getInstance().getApi().sendHeartbeat(gameId, localPlayer.isWhite()); + + DatabaseManager.getInstance().getApi() + .listenForHeartbeat( + gameId, + !localPlayer.isWhite(), // watch opponent's heartbeat + HEARTBEAT_TIMEOUT_MS, + latency -> Gdx.app.postRunnable(() -> { + updateConnectionUI(latency); + listener.onHeartbeatLatency(latency); + }), + () -> Gdx.app.postRunnable(() -> { + connectionIcon.setColor(Color.RED); + connectionLabel.setText("Lost"); + listener.onOpponentDisconnected(); + }) + ); + } + + // ───────────────────────────────────────────────────────────────────────── + // Connection UI + // ───────────────────────────────────────────────────────────────────────── + + private void updateConnectionUI(long latencyMs) { + if (latencyMs < 150) connectionIcon.setColor(Color.GREEN); + else if (latencyMs < 500) connectionIcon.setColor(Color.ORANGE); + else connectionIcon.setColor(Color.RED); + connectionLabel.setText(latencyMs + " ms"); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/game/GameOverlayManager.java b/core/src/main/java/com/group14/regicidechess/screens/game/GameOverlayManager.java new file mode 100644 index 0000000..d979d62 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/game/GameOverlayManager.java @@ -0,0 +1,200 @@ +package com.group14.regicidechess.screens.game; + +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.scenes.scene2d.InputEvent; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Image; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; +import com.badlogic.gdx.utils.Align; +import com.group14.regicidechess.model.Player; +import com.group14.regicidechess.utils.ResourceManager; + +/** + * GameOverlayManager — builds and controls all modal overlays in the game screen. + * + * Placement: core/src/main/java/com/group14/regicidechess/screens/game/GameOverlayManager.java + * + * Manages three overlays: + * 1. General overlay — forfeit confirmation and game-over summary. + * 2. Promotion overlay — piece selection when a pawn reaches the back rank. + * + * Callers supply callbacks; this class owns no game logic. + */ +public class GameOverlayManager { + + /** Callbacks surfaced to GameScreen. */ + public interface Listener { + void onForfeitConfirmed(); + void onGameOverBack(); + void onPromotionChosen(String pieceName); + } + + private final Stage stage; + private final Skin skin; + private final Player localPlayer; + private final Listener listener; + + // ── General overlay widgets ─────────────────────────────────────────────── + private Table overlayWrapper; + private Label overlayTitle; + private Label overlayBody; + private TextButton overlayConfirmBtn; + private TextButton overlayCancelBtn; + private Runnable onOverlayConfirm; + + // ── Promotion overlay ───────────────────────────────────────────────────── + private Table promotionOverlay; + + public GameOverlayManager(Stage stage, Skin skin, Player localPlayer, Listener listener) { + this.stage = stage; + this.skin = skin; + this.localPlayer = localPlayer; + this.listener = listener; + + buildGeneralOverlay(); + buildPromotionOverlay(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Visibility queries (used by GameScreen to block board taps) + // ───────────────────────────────────────────────────────────────────────── + + public boolean isAnyOverlayVisible() { + return overlayWrapper.isVisible() || promotionOverlay.isVisible(); + } + + public boolean isGeneralOverlayVisible() { + return overlayWrapper.isVisible(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Show helpers + // ───────────────────────────────────────────────────────────────────────── + + public void showForfeitConfirm() { + overlayTitle.setText("Forfeit?"); + overlayBody.setText("Are you sure you want to give up?"); + overlayConfirmBtn.setText("Yes, forfeit"); + overlayCancelBtn.setVisible(true); + onOverlayConfirm = listener::onForfeitConfirmed; + overlayWrapper.setVisible(true); + } + + public void showGameOver(boolean localWon) { + overlayTitle.setText(localWon ? "You Win!" : "Game Over"); + overlayBody.setText(localWon + ? "You captured the opponent's King!" + : "Your King was captured."); + overlayConfirmBtn.setText("Back to Menu"); + overlayCancelBtn.setVisible(false); + onOverlayConfirm = listener::onGameOverBack; + overlayWrapper.setVisible(true); + } + + public void showForfeitReceived(String reason) { + // reason = "forfeit:{loserColor}" + boolean opponentLost = reason.endsWith(localPlayer.isWhite() ? "black" : "white"); + overlayTitle.setText(opponentLost ? "You Win!" : "Game Over"); + overlayBody.setText(opponentLost ? "Opponent forfeited!" : "You forfeited."); + overlayConfirmBtn.setText("Back to Menu"); + overlayCancelBtn.setVisible(false); + onOverlayConfirm = listener::onGameOverBack; + overlayWrapper.setVisible(true); + } + + public void showPromotion() { + promotionOverlay.setVisible(true); + } + + // ───────────────────────────────────────────────────────────────────────── + // Construction + // ───────────────────────────────────────────────────────────────────────── + + private void buildGeneralOverlay() { + overlayWrapper = new Table(); + overlayWrapper.setFillParent(true); + overlayWrapper.setVisible(false); + stage.addActor(overlayWrapper); + + Table card = new Table(); + card.setBackground(skin.getDrawable("surface-pixel")); + card.pad(32); + + overlayTitle = new Label("", skin, "title"); + overlayTitle.setAlignment(Align.center); + overlayBody = new Label("", skin, "default"); + overlayBody.setAlignment(Align.center); + overlayBody.setWrap(true); + + overlayConfirmBtn = new TextButton("", skin, "accent"); + overlayCancelBtn = new TextButton("Cancel", skin, "default"); + + card.add(overlayTitle).expandX().padBottom(16).row(); + card.add(overlayBody).width(300).padBottom(32).row(); + + Table btnRow = new Table(); + btnRow.add(overlayCancelBtn).width(130).height(55).padRight(12); + btnRow.add(overlayConfirmBtn).width(130).height(55); + card.add(btnRow).row(); + + overlayWrapper.add(card).center(); + + overlayConfirmBtn.addListener(new ChangeListener() { + @Override public void changed(ChangeEvent event, Actor actor) { + overlayWrapper.setVisible(false); + if (onOverlayConfirm != null) onOverlayConfirm.run(); + } + }); + overlayCancelBtn.addListener(new ChangeListener() { + @Override public void changed(ChangeEvent event, Actor actor) { + overlayWrapper.setVisible(false); + } + }); + } + + private void buildPromotionOverlay() { + promotionOverlay = new Table(); + promotionOverlay.setFillParent(true); + promotionOverlay.setVisible(false); + stage.addActor(promotionOverlay); + + Table card = new Table(); + card.setBackground(skin.getDrawable("surface-pixel")); + card.pad(24); + + Label title = new Label("Promote Pawn", skin, "title"); + title.setAlignment(Align.center); + card.add(title).colspan(4).padBottom(20).row(); + + String color = localPlayer.isWhite() ? "white" : "black"; + String[] names = { "Queen", "Rook", "Bishop", "Knight" }; + + for (String pieceName : names) { + Table btn = new Table(); + btn.setBackground(skin.getDrawable("primary-pixel")); + btn.pad(8); + + Texture tex = ResourceManager.getInstance() + .getPieceTexture(color, pieceName.toLowerCase()); + btn.add(new Image(tex)).size(60).row(); + btn.add(new Label(pieceName, skin, "small")).row(); + + btn.setTouchable(com.badlogic.gdx.scenes.scene2d.Touchable.enabled); + btn.addListener(new ClickListener() { + @Override public void clicked(InputEvent event, float x, float y) { + promotionOverlay.setVisible(false); + listener.onPromotionChosen(pieceName); + } + }); + card.add(btn).size(100).pad(8); + } + + promotionOverlay.add(card).center(); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java b/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java new file mode 100644 index 0000000..eecfd4e --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java @@ -0,0 +1,452 @@ +package com.group14.regicidechess.screens.game; + +import com.badlogic.gdx.Game; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Image; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.utils.Align; +import com.badlogic.gdx.utils.viewport.FitViewport; +import com.group14.regicidechess.input.ScreenInputHandler; +import com.group14.regicidechess.model.Board; +import com.group14.regicidechess.model.Move; +import com.group14.regicidechess.model.Player; +import com.group14.regicidechess.model.pieces.ChessPiece; +import com.group14.regicidechess.model.pieces.King; +import com.group14.regicidechess.model.pieces.Pawn; +import com.group14.regicidechess.screens.game.GameBoardRenderer; +import com.group14.regicidechess.screens.game.GameNetworkHandler; +import com.group14.regicidechess.screens.game.GameOverlayManager; +import com.group14.regicidechess.screens.game.PieceFactory; +import com.group14.regicidechess.screens.mainmenu.MainMenuScreen; +import com.group14.regicidechess.states.InMatchState; +import com.group14.regicidechess.utils.ResourceManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * GameScreen — thin coordinator for an active chess match. + * + * Placement: core/src/main/java/com/group14/regicidechess/screens/GameScreen.java + * + * Responsibilities: + * - LibGDX Screen lifecycle (show, render, resize, dispose) + * - Building top-bar and status-bar UI + * - Board tap input and selection FSM + * - Delegating rendering → GameBoardRenderer + * - Delegating overlays → GameOverlayManager + * - Delegating Firebase I/O → GameNetworkHandler + * - Piece promotion logic → PieceFactory + */ +public class GameScreen implements Screen, + ScreenInputHandler.ScreenInputObserver, + GameOverlayManager.Listener, + GameNetworkHandler.Listener { + + // ── Layout ──────────────────────────────────────────────────────────────── + private static final float V_WIDTH = 480f; + private static final float V_HEIGHT = 854f; + private static final float TOP_BAR_HEIGHT = 70f; + private static final float STATUS_BAR_HEIGHT = 60f; + private static final float HEARTBEAT_INTERVAL = 5f; + + // ── LibGDX ──────────────────────────────────────────────────────────────── + private final Game game; + private final SpriteBatch batch; + private final Stage stage; + private final Skin skin; + private final ScreenInputHandler inputHandler; + + // ── Game state ──────────────────────────────────────────────────────────── + private final InMatchState inMatchState; + private final Player localPlayer; + private final String gameId; + private final int boardSize; + + // ── Selection FSM (logic coordinates) ──────────────────────────────────── + private Vector2 selectedCell = null; + private List validMoves = new ArrayList<>(); + + // ── Pending promotion ───────────────────────────────────────────────────── + private Move pendingPromotionMove = null; + + // ── Helpers ─────────────────────────────────────────────────────────────── + private final GameBoardRenderer boardRenderer; + private final GameOverlayManager overlayManager; + private final GameNetworkHandler networkHandler; + + // ── Widgets ─────────────────────────────────────────────────────────────── + private Label turnLabel; + private Label statusLabel; + private float heartbeatTimer = 0f; + + // ───────────────────────────────────────────────────────────────────────── + // Constructor + // ───────────────────────────────────────────────────────────────────────── + + public GameScreen(Game game, SpriteBatch batch, + Board board, Player localPlayer, int boardSize, String gameId) { + this.game = game; + this.batch = batch; + this.localPlayer = localPlayer; + this.gameId = gameId; + this.boardSize = boardSize; + + Player opponent = new Player( + localPlayer.isWhite() ? "player2" : "player1", + !localPlayer.isWhite(), + localPlayer.getBudget()); + + inMatchState = new InMatchState(); + inMatchState.init(board, + localPlayer.isWhite() ? localPlayer : opponent, + localPlayer.isWhite() ? opponent : localPlayer); + inMatchState.enter(); + + stage = new Stage(new FitViewport(V_WIDTH, V_HEIGHT), batch); + skin = ResourceManager.getInstance().getSkin(); + inputHandler = new ScreenInputHandler(); + inputHandler.addObserver(this); + + // Connection UI elements are owned here but passed to the network handler + Image connectionIcon = new Image(skin.getDrawable("white-pixel")); + Label connectionLabel = new Label("Connecting", skin, "small"); + connectionIcon.setColor(Color.GRAY); + + buildUI(connectionIcon, connectionLabel); + + overlayManager = new GameOverlayManager(stage, skin, localPlayer, this); + networkHandler = new GameNetworkHandler(gameId, localPlayer, this, + connectionIcon, connectionLabel); + boardRenderer = new GameBoardRenderer(batch, localPlayer, boardSize); + boardRenderer.computeGeometry(V_WIDTH, V_HEIGHT, TOP_BAR_HEIGHT, STATUS_BAR_HEIGHT); + + networkHandler.start(); + refreshTurnLabel(); + } + + // ───────────────────────────────────────────────────────────────────────── + // UI construction + // ───────────────────────────────────────────────────────────────────────── + + private void buildUI(Image connectionIcon, Label connectionLabel) { + Table root = new Table(); + root.setFillParent(true); + root.top(); + stage.addActor(root); + + // Top bar + Table topBar = new Table(); + topBar.setBackground(skin.getDrawable("primary-pixel")); + topBar.pad(10); + + TextButton forfeitBtn = new TextButton("Forfeit", skin, "danger"); + forfeitBtn.addListener(new ChangeListener() { + @Override public void changed(ChangeEvent event, Actor actor) { + overlayManager.showForfeitConfirm(); + } + }); + + turnLabel = new Label("", skin, "title"); + turnLabel.setAlignment(Align.center); + + Table connGroup = new Table(); + connGroup.add(connectionIcon).size(12, 12).padRight(6); + connGroup.add(connectionLabel).right(); + + topBar.add(forfeitBtn).width(100).height(50).left(); + topBar.add(turnLabel).expandX().center(); + topBar.add(connGroup).width(110).right().padRight(8); + + root.add(topBar).expandX().fillX().height(TOP_BAR_HEIGHT).row(); + root.add().expandX().expandY().row(); // spacer for board area + + // Status bar + Table statusBar = new Table(); + statusBar.setBackground(skin.getDrawable("surface-pixel")); + statusBar.pad(10); + statusLabel = new Label("Select a piece to move.", skin, "small"); + statusLabel.setAlignment(Align.center); + statusBar.add(statusLabel).expandX(); + root.add(statusBar).expandX().fillX().height(STATUS_BAR_HEIGHT).row(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Rendering + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void render(float delta) { + Gdx.gl.glClearColor(0.12f, 0.12f, 0.15f, 1f); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + inMatchState.update(delta); + + heartbeatTimer += delta; + if (heartbeatTimer >= HEARTBEAT_INTERVAL) { + heartbeatTimer = 0f; + networkHandler.sendHeartbeat(); + } + + boardRenderer.draw(stage.getCamera().combined, + inMatchState.getBoard(), selectedCell, validMoves); + + stage.act(delta); + stage.draw(); + + // Draw a semi-transparent dimmer behind any visible overlay + if (overlayManager.isAnyOverlayVisible()) { + Gdx.gl.glEnable(GL20.GL_BLEND); + Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); + ShapeRenderer sr = new ShapeRenderer(); + sr.setProjectionMatrix(stage.getCamera().combined); + sr.begin(ShapeRenderer.ShapeType.Filled); + sr.setColor(0f, 0f, 0f, 0.65f); + sr.rect(0, 0, V_WIDTH, V_HEIGHT); + sr.end(); + sr.dispose(); + Gdx.gl.glDisable(GL20.GL_BLEND); + stage.draw(); // redraw so overlay card appears above dimmer + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Input — ScreenInputObserver + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void onTap(int screenX, int screenY, int pointer, int button) { + if (overlayManager.isAnyOverlayVisible()) return; + Vector2 world = stage.getViewport().unproject(new Vector2(screenX, screenY)); + handleBoardTap(world.x, world.y); + } + @Override public void onDrag (int x, int y, int pointer) {} + @Override public void onRelease(int x, int y, int pointer, int button) {} + @Override public void onKeyDown(int keycode) {} + + private void handleBoardTap(float worldX, float worldY) { + if (!inMatchState.isMyTurn(localPlayer)) { + showStatus("Not your turn."); + return; + } + + float cellSize = boardRenderer.getCellSize(); + float boardLeft = boardRenderer.getBoardLeft(); + float boardBot = boardRenderer.getBoardBottom(); + int size = inMatchState.getBoard().getSize(); + + if (worldX < boardLeft || worldX > boardLeft + size * cellSize) return; + if (worldY < boardBot || worldY > boardBot + size * cellSize) return; + + int col = Math.max(0, Math.min((int)((worldX - boardLeft) / cellSize), size - 1)); + int dispRow = Math.max(0, Math.min((int)((worldY - boardBot) / cellSize), size - 1)); + int logicRow = boardRenderer.toLogicRow(dispRow); + + Vector2 tapped = new Vector2(col, logicRow); + + if (selectedCell == null) { + trySelect(tapped); + } else if (containsVector(validMoves, tapped)) { + executeMove(selectedCell, tapped); + } else { + ChessPiece occupant = inMatchState.getBoard().getPieceAt(col, logicRow); + if (occupant != null && occupant.getOwner() == localPlayer) { + trySelect(tapped); + } else { + deselect(); + } + } + } + + private void trySelect(Vector2 cell) { + ChessPiece piece = inMatchState.getBoard().getPieceAt((int) cell.x, (int) cell.y); + if (piece == null || piece.getOwner() != localPlayer) { + showStatus("Select one of your own pieces."); + deselect(); + return; + } + selectedCell = cell; + validMoves = piece.validMoves(); + showStatus("Selected: " + piece.getTypeName() + " — " + validMoves.size() + " move(s)."); + } + + private void deselect() { + selectedCell = null; + validMoves.clear(); + showStatus("Select a piece to move."); + } + + // ───────────────────────────────────────────────────────────────────────── + // Move execution + // ───────────────────────────────────────────────────────────────────────── + + private void executeMove(Vector2 from, Vector2 to) { + ChessPiece movingPiece = inMatchState.getBoard().getPieceAt(from); + if (movingPiece == null) { deselect(); return; } + + // Execute locally — this also switches the turn inside InMatchState + ChessPiece captured = inMatchState.executeMove(from, to); + deselect(); + + if (captured instanceof King) { + networkHandler.saveMove(new Move(from, to, movingPiece, localPlayer)); + refreshTurnLabel(); + overlayManager.showGameOver(true); + + } else if (isPawnPromotion(movingPiece, to)) { + // Do NOT save yet — wait for the player to pick a promotion piece + pendingPromotionMove = new Move(from, to, movingPiece, localPlayer); + overlayManager.showPromotion(); + + } else { + networkHandler.saveMove(new Move(from, to, movingPiece, localPlayer)); + refreshTurnLabel(); + showStatus("Waiting for opponent..."); + } + } + + private boolean isPawnPromotion(ChessPiece piece, Vector2 to) { + if (!(piece instanceof Pawn)) return false; + int rank = piece.getOwner().isWhite() ? inMatchState.getBoard().getSize() - 1 : 0; + return (int) to.y == rank; + } + + // ───────────────────────────────────────────────────────────────────────── + // GameOverlayManager.Listener + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void onForfeitConfirmed() { + networkHandler.signalForfeit(); + overlayManager.showGameOver(false); + } + + @Override + public void onGameOverBack() { + game.setScreen(new MainMenuScreen(game, batch)); + } + + /** + * Promotion piece chosen by the local player. + * Replaces the pawn on the board and sends the complete move to Firebase. + * Turn was already switched by executeMove(); just refresh the label. + */ + @Override + public void onPromotionChosen(String pieceName) { + if (pendingPromotionMove == null) return; + + ChessPiece promoted = PieceFactory.fromName(pieceName, localPlayer); + Vector2 toCell = pendingPromotionMove.getTo(); + inMatchState.getBoard().placePiece(promoted, (int) toCell.x, (int) toCell.y); + + networkHandler.saveMove(new Move( + pendingPromotionMove.getFrom(), + pendingPromotionMove.getTo(), + pendingPromotionMove.getPiece(), + localPlayer, + pieceName + )); + + refreshTurnLabel(); + showStatus("Waiting for opponent..."); + pendingPromotionMove = null; + } + + // ───────────────────────────────────────────────────────────────────────── + // GameNetworkHandler.Listener + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void onOpponentMove(Vector2 from, Vector2 to, int promoCode) { + ChessPiece piece = inMatchState.getBoard().getPieceAt(from); + if (piece == null || piece.getOwner() == localPlayer) return; + + Player opponent = piece.getOwner(); + ChessPiece captured = inMatchState.executeMove(from, to); + + if (promoCode != 0) { + ChessPiece promoted = PieceFactory.fromCode(promoCode, opponent); + if (promoted != null) { + inMatchState.getBoard().placePiece(promoted, (int) to.x, (int) to.y); + } + } + + deselect(); + refreshTurnLabel(); + + if (captured instanceof King) overlayManager.showGameOver(false); + else showStatus("Your turn!"); + } + + @Override + public void onGameOver(String reason) { + if (!overlayManager.isGeneralOverlayVisible()) { + overlayManager.showForfeitReceived(reason); + } + } + + @Override public void onHeartbeatLatency(long latencyMs) { /* handled by GameNetworkHandler */ } + + @Override + public void onOpponentDisconnected() { + showStatus("Opponent disconnected."); + } + + // ───────────────────────────────────────────────────────────────────────── + // Helpers + // ───────────────────────────────────────────────────────────────────────── + + private void refreshTurnLabel() { + turnLabel.setText(inMatchState.isMyTurn(localPlayer) ? "Your turn" : "Opponent's turn"); + } + + private void showStatus(String msg) { statusLabel.setText(msg); } + + private boolean containsVector(List list, Vector2 v) { + for (Vector2 item : list) + if ((int) item.x == (int) v.x && (int) item.y == (int) v.y) return true; + return false; + } + + // ───────────────────────────────────────────────────────────────────────── + // Screen lifecycle + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void show() { + Gdx.input.setInputProcessor( + new com.badlogic.gdx.InputMultiplexer(stage, inputHandler)); + } + + @Override + public void resize(int width, int height) { + stage.getViewport().update(width, height, true); + boardRenderer.computeGeometry(V_WIDTH, V_HEIGHT, TOP_BAR_HEIGHT, STATUS_BAR_HEIGHT); + } + + @Override public void pause() {} + @Override public void resume() {} + + @Override + public void hide() { + inputHandler.clearObservers(); + inMatchState.exit(); + } + + @Override + public void dispose() { + stage.dispose(); + boardRenderer.dispose(); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/game/PieceFactory.java b/core/src/main/java/com/group14/regicidechess/screens/game/PieceFactory.java new file mode 100644 index 0000000..b8418e5 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/game/PieceFactory.java @@ -0,0 +1,59 @@ +package com.group14.regicidechess.screens.game; + +import com.group14.regicidechess.model.Player; +import com.group14.regicidechess.model.pieces.Bishop; +import com.group14.regicidechess.model.pieces.ChessPiece; +import com.group14.regicidechess.model.pieces.Knight; +import com.group14.regicidechess.model.pieces.Queen; +import com.group14.regicidechess.model.pieces.Rook; + +/** + * PieceFactory — stateless helpers for building promotion pieces. + * + * Placement: core/src/main/java/com/group14/regicidechess/screens/game/PieceFactory.java + * + * Promotion codes (shared between GameScreen and AndroidFirebase): + * 2 = Queen, 3 = Rook, 4 = Bishop, 5 = Knight + */ +public final class PieceFactory { + + private PieceFactory() {} + + /** + * Builds a promotion piece from a display name. + * Falls back to Knight for unrecognised names. + */ + public static ChessPiece fromName(String name, Player owner) { + switch (name) { + case "Queen": return new Queen(owner); + case "Rook": return new Rook(owner); + case "Bishop": return new Bishop(owner); + default: return new Knight(owner); + } + } + + /** + * Builds a promotion piece from a Firebase promotion code. + * Returns null for code 0 (no promotion). + */ + public static ChessPiece fromCode(int code, Player owner) { + switch (code) { + case 2: return new Queen(owner); + case 3: return new Rook(owner); + case 4: return new Bishop(owner); + case 5: return new Knight(owner); + default: return null; + } + } + + /** Returns the Firebase promotion code for a piece name. */ + public static int toCode(String name) { + switch (name) { + case "Queen": return 2; + case "Rook": return 3; + case "Bishop": return 4; + case "Knight": return 5; + default: return 0; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java new file mode 100644 index 0000000..93d7d20 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java @@ -0,0 +1,67 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java +package com.group14.regicidechess.screens.lobby; + +import com.badlogic.gdx.Gdx; +import com.group14.regicidechess.database.DatabaseManager; + +/** + * Manages Firebase listeners and flow between host and joiner. + * Encapsulates all Firebase communication logic. + */ +public class LobbyFlowController { + + public interface FlowListener { + void onJoinerArrived(); // Host: someone joined the lobby + void onGameStarted(); // Joiner: host started the game + void onError(String message); + } + + private final FlowListener listener; + private String activeGameId; + + public LobbyFlowController(FlowListener listener) { + this.listener = listener; + } + + /** + * Host: Listens for a joiner to enter the lobby. + * When status becomes "joined", calls onJoinerArrived(). + */ + public void listenForJoiner(String gameId) { + this.activeGameId = gameId; + DatabaseManager.getInstance().getApi() + .listenForOpponentReady(gameId, + () -> Gdx.app.postRunnable(() -> { + if (listener != null) { + listener.onJoinerArrived(); + } + })); + } + + /** + * Host: Signals the game to start. + * Writes status = "started" to Firebase. + */ + public void signalGameStart(String gameId) { + DatabaseManager.getInstance().getApi().startGame(gameId); + } + + /** + * Joiner: Listens for host to start the game. + * When status becomes "started", calls onGameStarted(). + */ + public void listenForGameStart(String gameId) { + this.activeGameId = gameId; + DatabaseManager.getInstance().getApi() + .listenForGameStart(gameId, + () -> Gdx.app.postRunnable(() -> { + if (listener != null) { + listener.onGameStarted(); + } + })); + } + + public String getActiveGameId() { + return activeGameId; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyHostUI.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyHostUI.java new file mode 100644 index 0000000..24fb3d6 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyHostUI.java @@ -0,0 +1,196 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyHostUI.java +package com.group14.regicidechess.screens.lobby; + +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Slider; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.utils.Align; + +/** + * Builds and manages the host-specific UI for lobby creation. + * Contains board size slider, budget slider, create button, and start button. + */ +public class LobbyHostUI { + + public interface HostUIListener { + void onCreateLobby(int boardSize, int budget); + void onStartGame(); + void onBack(); + } + + private final Skin skin; + private final HostUIListener listener; + + private int boardSize = LobbyScreenConfig.BOARD_DEFAULT; + private int budget = LobbyScreenConfig.BUDGET_DEFAULT; + + private Label boardSizeValueLabel; + private Label budgetValueLabel; + private Label statusLabel; + private TextButton createBtn; + private TextButton startBtn; + + public LobbyHostUI(Skin skin, HostUIListener listener) { + this.skin = skin; + this.listener = listener; + } + + public Table build() { + Table container = new Table(); + container.top(); + + // Board size controls + Label boardSizeLabel = new Label("Board Size", skin); + boardSizeValueLabel = new Label(boardSize + " x " + boardSize, skin); + + Slider boardSlider = createBoardSlider(); + + // Budget controls + Label budgetLabel = new Label("Starting Budget", skin); + budgetValueLabel = new Label(String.valueOf(budget), skin); + + Slider budgetSlider = createBudgetSlider(); + + // Status label + statusLabel = new Label("", skin, "small"); + statusLabel.setAlignment(Align.center); + + // Buttons + createBtn = new TextButton("Create Lobby", skin, "accent"); + createBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + if (!createBtn.isDisabled()) { + listener.onCreateLobby(boardSize, budget); + } + } + }); + + startBtn = new TextButton("Start Game", skin, "accent"); + startBtn.setVisible(false); + startBtn.setDisabled(true); + startBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + if (!startBtn.isDisabled()) { + listener.onStartGame(); + } + } + }); + + // Layout + container.add(buildRow(boardSizeLabel, boardSizeValueLabel)) + .expandX().fillX().padBottom(8).row(); + container.add(boardSlider) + .width(LobbyScreenConfig.SLIDER_WIDTH) + .height(LobbyScreenConfig.SLIDER_HEIGHT) + .padBottom(24).row(); + container.add(buildRow(budgetLabel, budgetValueLabel)) + .expandX().fillX().padBottom(8).row(); + container.add(budgetSlider) + .width(LobbyScreenConfig.SLIDER_WIDTH) + .height(LobbyScreenConfig.SLIDER_HEIGHT) + .padBottom(32).row(); + container.add(createBtn) + .width(LobbyScreenConfig.BUTTON_WIDTH) + .height(LobbyScreenConfig.BUTTON_HEIGHT) + .padBottom(8).row(); + container.add(startBtn) + .width(LobbyScreenConfig.BUTTON_WIDTH) + .height(LobbyScreenConfig.BUTTON_HEIGHT) + .padBottom(16).row(); + container.add(statusLabel).expandX().row(); + + return container; + } + + private Slider createBoardSlider() { + Slider slider = new Slider( + LobbyScreenConfig.BOARD_MIN, + LobbyScreenConfig.BOARD_MAX, + 1, + false, + buildSliderStyle()); + slider.setValue(LobbyScreenConfig.BOARD_DEFAULT); + slider.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + boardSize = (int) slider.getValue(); + boardSizeValueLabel.setText(boardSize + " x " + boardSize); + } + }); + return slider; + } + + private Slider createBudgetSlider() { + Slider slider = new Slider( + LobbyScreenConfig.BUDGET_MIN, + LobbyScreenConfig.BUDGET_MAX, + LobbyScreenConfig.BUDGET_STEP, + false, + buildSliderStyle()); + slider.setValue(LobbyScreenConfig.BUDGET_DEFAULT); + slider.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + budget = (int) slider.getValue(); + budgetValueLabel.setText(String.valueOf(budget)); + } + }); + return slider; + } + + private Slider.SliderStyle buildSliderStyle() { + Slider.SliderStyle style = new Slider.SliderStyle(); + style.background = skin.getDrawable("primary-pixel"); + style.knob = skin.getDrawable("accent-pixel"); + return style; + } + + private Table buildRow(Label left, Label right) { + Table row = new Table(); + row.add(left).expandX().left(); + row.add(right).expandX().right(); + return row; + } + + // Public methods for updating UI state + + public void showCreatingState() { + createBtn.setDisabled(true); + setStatus("Creating lobby..."); + } + + public void showLobbyCreated(String gameId) { + createBtn.setVisible(false); + startBtn.setVisible(true); + startBtn.setDisabled(true); + setStatus("Lobby created!\nGame ID: " + gameId + + "\n\nWaiting for opponent to join..."); + } + + public void showJoinerArrived() { + setStatus("Opponent joined! Press Start Game when you are ready."); + startBtn.setDisabled(false); + } + + public void showStartingState() { + startBtn.setDisabled(true); + setStatus("Starting..."); + } + + public void showCreationFailed() { + setStatus("Failed to create lobby. Try again."); + createBtn.setDisabled(false); + } + + public void setStatus(String message) { + if (statusLabel != null) { + statusLabel.setText(message); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyJoinUI.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyJoinUI.java new file mode 100644 index 0000000..97b470a --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyJoinUI.java @@ -0,0 +1,117 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyJoinUI.java +package com.group14.regicidechess.screens.lobby; + +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.utils.Align; + +/** + * Builds and manages the join-specific UI for joining an existing lobby. + * Contains game ID display, board/budget info, and join button. + */ +public class LobbyJoinUI { + + public interface JoinUIListener { + void onJoin(String gameId); + void onBack(); + } + + private final Skin skin; + private final JoinUIListener listener; + private final String gameId; + + private Label boardInfoLabel; + private Label budgetInfoLabel; + private Label statusLabel; + private TextButton joinBtn; + + private int boardSize = -1; + private int budget = -1; + + public LobbyJoinUI(Skin skin, String gameId, JoinUIListener listener) { + this.skin = skin; + this.gameId = gameId; + this.listener = listener; + } + + public Table build() { + Table container = new Table(); + container.top(); + + // Game ID display + Label gameIdLabel = new Label("Game ID: " + gameId, skin, "title"); + gameIdLabel.setAlignment(Align.center); + + // Board and budget info (initially fetching) + boardInfoLabel = new Label("Board: fetching...", skin, "small"); + budgetInfoLabel = new Label("Budget: fetching...", skin, "small"); + + // Status label + statusLabel = new Label("", skin, "small"); + statusLabel.setAlignment(Align.center); + + // Join button + joinBtn = new TextButton("Join Match", skin, "accent"); + joinBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + if (!joinBtn.isDisabled()) { + listener.onJoin(gameId); + } + } + }); + + // Layout + container.add(gameIdLabel).expandX().padBottom(24).row(); + container.add(boardInfoLabel).expandX().padBottom(4).row(); + container.add(budgetInfoLabel).expandX().padBottom(32).row(); + container.add(joinBtn) + .width(LobbyScreenConfig.BUTTON_WIDTH) + .height(LobbyScreenConfig.BUTTON_HEIGHT) + .padBottom(16).row(); + container.add(statusLabel).expandX().row(); + + return container; + } + + // Public methods for updating UI state + + public void updateLobbyInfo(int boardSize, int budget) { + this.boardSize = boardSize; + this.budget = budget; + boardInfoLabel.setText("Board: " + boardSize + " x " + boardSize); + budgetInfoLabel.setText("Budget: " + budget); + } + + public void showJoiningState() { + joinBtn.setDisabled(true); + setStatus("Connecting..."); + } + + public void showJoinedState() { + setStatus("Joined!\nWaiting for host to start the game..."); + } + + public void showJoinFailed() { + setStatus("Lobby not found. Check the Game ID."); + joinBtn.setDisabled(false); + } + + public void setStatus(String message) { + if (statusLabel != null) { + statusLabel.setText(message); + } + } + + public int getBoardSize() { + return boardSize; + } + + public int getBudget() { + return budget; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyMode.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyMode.java new file mode 100644 index 0000000..8b4a249 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyMode.java @@ -0,0 +1,10 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyMode.java +package com.group14.regicidechess.screens.lobby; + +/** + * Defines the two modes for the lobby screen. + */ +public enum LobbyMode { + HOST, // Player creating a new lobby + JOIN // Player joining an existing lobby +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java new file mode 100644 index 0000000..8259b72 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java @@ -0,0 +1,301 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java +package com.group14.regicidechess.screens.lobby; + +import com.badlogic.gdx.Game; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.utils.viewport.FitViewport; +import com.group14.regicidechess.input.ScreenInputHandler; +import com.group14.regicidechess.model.Lobby; +import com.group14.regicidechess.screens.mainmenu.MainMenuScreen; +import com.group14.regicidechess.screens.setup.SetupScreen; +import com.group14.regicidechess.utils.ResourceManager; + +/** + * LobbyScreen — thin coordinator for lobby creation and joining. + * + * Refactored version with improved modularity: + * - UI components extracted to LobbyHostUI and LobbyJoinUI + * - Firebase flow extracted to LobbyFlowController + * - State management extracted to LobbyStateManager + * - Screen now focuses purely on coordination between components + */ +public class LobbyScreen implements Screen, ScreenInputHandler.ScreenInputObserver { + + // LibGDX + private final Game game; + private final SpriteBatch batch; + private final Stage stage; + private final Skin skin; + private final ScreenInputHandler inputHandler; + + // Mode and data + private final LobbyMode mode; + private final String incomingGameId; + + // Components + private final LobbyStateManager stateManager; + private final LobbyFlowController flowController; + private final LobbyHostUI hostUI; + private final LobbyJoinUI joinUI; + + // Shared UI + private Label titleLabel; + private Table root; + + public LobbyScreen(Game game, SpriteBatch batch, LobbyMode mode, Lobby lobby) { + this.game = game; + this.batch = batch; + this.mode = mode; + this.incomingGameId = lobby != null ? lobby.getGameId() : null; + + // Initialize components + this.stateManager = new LobbyStateManager(); + if (lobby != null) { + stateManager.setPrefetchedLobby(lobby); + } + + this.flowController = new LobbyFlowController(createFlowListener()); + this.stateManager.setListener(createStateListener()); + + // LibGDX setup + this.stage = new Stage(new FitViewport( + LobbyScreenConfig.VIEWPORT_WIDTH, + LobbyScreenConfig.VIEWPORT_HEIGHT), + batch); + this.skin = ResourceManager.getInstance().getSkin(); + this.inputHandler = new ScreenInputHandler(); + inputHandler.addObserver(this); + + // Create mode-specific UI + if (mode == LobbyMode.HOST) { + this.hostUI = new LobbyHostUI(skin, createHostUIListener()); + this.joinUI = null; + } else { + this.joinUI = new LobbyJoinUI(skin, incomingGameId, createJoinUIListener()); + this.hostUI = null; + } + + stateManager.enter(); + buildUI(); + + // If we have prefetched lobby data for joiner, update UI immediately + if (mode == LobbyMode.JOIN && lobby != null) { + joinUI.updateLobbyInfo(lobby.getBoardSize(), lobby.getBudget()); + } + } + + // ───────────────────────────────────────────────────────────────────────── + // UI construction + // ───────────────────────────────────────────────────────────────────────── + + private void buildUI() { + root = new Table(); + root.setFillParent(true); + root.setBackground(skin.getDrawable("surface-pixel")); + root.top().pad(32); + stage.addActor(root); + + // Title + String titleText = mode == LobbyMode.HOST ? "Create Lobby" : "Join Lobby"; + titleLabel = new Label(titleText, skin, "title"); + titleLabel.setAlignment(com.badlogic.gdx.utils.Align.center); + root.add(titleLabel).expandX().padBottom(32).row(); + + // Mode-specific content + if (mode == LobbyMode.HOST) { + root.add(hostUI.build()).expandX().fillX().row(); + } else { + root.add(joinUI.build()).expandX().fillX().row(); + } + + // Back button (shared) + TextButton backBtn = new TextButton("Back", skin, "default"); + backBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + onBack(); + } + }); + root.add(backBtn) + .width(LobbyScreenConfig.BACK_BUTTON_WIDTH) + .height(LobbyScreenConfig.BACK_BUTTON_HEIGHT) + .padTop(24).row(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Listeners + // ───────────────────────────────────────────────────────────────────────── + + private LobbyHostUI.HostUIListener createHostUIListener() { + return new LobbyHostUI.HostUIListener() { + @Override + public void onCreateLobby(int boardSize, int budget) { + hostUI.showCreatingState(); + stateManager.setBoardSize(boardSize); + stateManager.setBudget(budget); + stateManager.createLobby(); + } + + @Override + public void onStartGame() { + hostUI.showStartingState(); + String gameId = stateManager.getGameId(); + flowController.signalGameStart(gameId); + navigateToSetup(gameId, true); + } + + @Override + public void onBack() { + game.setScreen(new MainMenuScreen(game, batch)); + } + }; + } + + private LobbyJoinUI.JoinUIListener createJoinUIListener() { + return new LobbyJoinUI.JoinUIListener() { + @Override + public void onJoin(String gameId) { + joinUI.showJoiningState(); + stateManager.joinLobby(gameId); + } + + @Override + public void onBack() { + game.setScreen(new MainMenuScreen(game, batch)); + } + }; + } + + private LobbyStateManager.StateListener createStateListener() { + return new LobbyStateManager.StateListener() { + @Override + public void onLobbyCreated(String gameId) { + hostUI.showLobbyCreated(gameId); + flowController.listenForJoiner(gameId); + } + + @Override + public void onLobbyCreationFailed() { + hostUI.showCreationFailed(); + } + + @Override + public void onLobbyJoined() { + joinUI.updateLobbyInfo( + stateManager.getBoardSize(), + stateManager.getBudget()); + joinUI.showJoinedState(); + flowController.listenForGameStart(stateManager.getGameId()); + } + + @Override + public void onLobbyJoinFailed() { + joinUI.showJoinFailed(); + } + }; + } + + private LobbyFlowController.FlowListener createFlowListener() { + return new LobbyFlowController.FlowListener() { + @Override + public void onJoinerArrived() { + hostUI.showJoinerArrived(); + } + + @Override + public void onGameStarted() { + navigateToSetup(stateManager.getGameId(), false); + } + + @Override + public void onError(String message) { + if (mode == LobbyMode.HOST) { + hostUI.setStatus(message); + } else { + joinUI.setStatus(message); + } + } + }; + } + + // ───────────────────────────────────────────────────────────────────────── + // Navigation + // ───────────────────────────────────────────────────────────────────────── + + private void navigateToSetup(String gameId, boolean isHost) { + int boardSize = stateManager.getBoardSize(); + int budget = stateManager.getBudget(); + game.setScreen(new SetupScreen(game, batch, gameId, boardSize, budget, isHost)); + } + + private void onBack() { + game.setScreen(new MainMenuScreen(game, batch)); + } + + // ───────────────────────────────────────────────────────────────────────── + // Screen lifecycle + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void show() { + Gdx.input.setInputProcessor( + new com.badlogic.gdx.InputMultiplexer(stage, inputHandler)); + } + + @Override + public void render(float delta) { + Gdx.gl.glClearColor(0.12f, 0.12f, 0.15f, 1f); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + stateManager.update(delta); + stage.act(delta); + stage.draw(); + } + + @Override + public void resize(int width, int height) { + stage.getViewport().update(width, height, true); + } + + @Override + public void pause() {} + + @Override + public void resume() {} + + @Override + public void hide() { + inputHandler.clearObservers(); + stateManager.exit(); + } + + @Override + public void dispose() { + stage.dispose(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Input handling (unused but required by interface) + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void onTap(int x, int y, int pointer, int button) {} + + @Override + public void onDrag(int x, int y, int pointer) {} + + @Override + public void onRelease(int x, int y, int pointer, int button) {} + + @Override + public void onKeyDown(int keycode) {} +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreenConfig.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreenConfig.java new file mode 100644 index 0000000..44e0d18 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreenConfig.java @@ -0,0 +1,32 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreenConfig.java +package com.group14.regicidechess.screens.lobby; + +/** + * Configuration constants for the lobby screen. + */ +public final class LobbyScreenConfig { + private LobbyScreenConfig() {} // Prevent instantiation + + // Viewport dimensions + public static final float VIEWPORT_WIDTH = 480f; + public static final float VIEWPORT_HEIGHT = 854f; + + // Board size limits + public static final int BOARD_MIN = 5; + public static final int BOARD_MAX = 8; + public static final int BOARD_DEFAULT = 8; + + // Budget limits + public static final int BUDGET_MIN = 5; + public static final int BUDGET_MAX = 50; + public static final int BUDGET_DEFAULT = 25; + public static final int BUDGET_STEP = 1; + + // UI dimensions + public static final int SLIDER_WIDTH = 380; + public static final int SLIDER_HEIGHT = 40; + public static final int BUTTON_WIDTH = 280; + public static final int BUTTON_HEIGHT = 60; + public static final int BACK_BUTTON_WIDTH = 200; + public static final int BACK_BUTTON_HEIGHT = 50; +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyStateManager.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyStateManager.java new file mode 100644 index 0000000..ac37d51 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyStateManager.java @@ -0,0 +1,123 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyStateManager.java +package com.group14.regicidechess.screens.lobby; + +import com.group14.regicidechess.model.Lobby; +import com.group14.regicidechess.states.LobbyState; + +/** + * Manages lobby state and provides a clean interface for state operations. + * Encapsulates LobbyState and provides methods for host and join operations. + */ +public class LobbyStateManager { + + public interface StateListener { + void onLobbyCreated(String gameId); + void onLobbyCreationFailed(); + void onLobbyJoined(); + void onLobbyJoinFailed(); + } + + private final LobbyState lobbyState; + private StateListener listener; + + private int boardSize; + private int budget; + private String gameId; + + public LobbyStateManager() { + this.lobbyState = new LobbyState(); + this.boardSize = LobbyScreenConfig.BOARD_DEFAULT; + this.budget = LobbyScreenConfig.BUDGET_DEFAULT; + } + + public void setListener(StateListener listener) { + this.listener = listener; + } + + public void enter() { + lobbyState.enter(); + } + + public void exit() { + lobbyState.exit(); + } + + public void update(float delta) { + lobbyState.update(delta); + } + + // Host methods + + public void setBoardSize(int boardSize) { + this.boardSize = boardSize; + } + + public void setBudget(int budget) { + this.budget = budget; + } + + public void createLobby() { + lobbyState.createLobby(boardSize, budget, + () -> { + if (listener != null) { + listener.onLobbyCreated(lobbyState.getLobby().getGameId()); + } + }, + () -> { + if (listener != null) { + listener.onLobbyCreationFailed(); + } + }); + } + + // Join methods + + public void setPrefetchedLobby(Lobby lobby) { + if (lobby != null) { + lobbyState.setPrefetchedLobby(lobby); + this.boardSize = lobby.getBoardSize(); + this.budget = lobby.getBudget(); + this.gameId = lobby.getGameId(); + } + } + + public void joinLobby(String gameId) { + this.gameId = gameId; + lobbyState.joinLobby(gameId, + () -> { + if (listener != null) { + listener.onLobbyJoined(); + } + }, + () -> { + if (listener != null) { + listener.onLobbyJoinFailed(); + } + }); + } + + // Getters + + public Lobby getLobby() { + return lobbyState.getLobby(); + } + + public int getBoardSize() { + return lobbyState.getLobby() != null ? + lobbyState.getLobby().getBoardSize() : boardSize; + } + + public int getBudget() { + return lobbyState.getLobby() != null ? + lobbyState.getLobby().getBudget() : budget; + } + + public String getGameId() { + return lobbyState.getLobby() != null ? + lobbyState.getLobby().getGameId() : gameId; + } + + public LobbyState getLobbyState() { + return lobbyState; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/JoinGamePanel.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/JoinGamePanel.java new file mode 100644 index 0000000..959ef26 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/JoinGamePanel.java @@ -0,0 +1,186 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/mainmenu/JoinGamePanel.java +package com.group14.regicidechess.screens.mainmenu; + +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.ui.TextField; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.group14.regicidechess.model.Lobby; + +/** + * Join game panel UI component. + * Handles the panel that appears when "Join Lobby" is pressed. + */ +public class JoinGamePanel { + + public interface JoinPanelListener { + void onJoin(String gameId); + void onCancel(); + void onError(String message); + } + + private final Skin skin; + private final JoinPanelListener listener; + private final LobbyValidator validator; + + private Table panel; + private TextField gameIdField; + private TextButton joinConfirmBtn; + private Label errorLabel; + + private boolean visible = false; + + public JoinGamePanel(Skin skin, JoinPanelListener listener) { + this.skin = skin; + this.listener = listener; + this.validator = new LobbyValidator(createValidationListener()); + buildPanel(); + } + + private void buildPanel() { + panel = new Table(); + panel.setBackground(skin.getDrawable("primary-pixel")); + panel.pad(MainMenuConfig.JOIN_PANEL_PAD); + + Label hint = new Label("Enter Game ID", skin, "small"); + gameIdField = new TextField("", skin); + gameIdField.setMessageText("e.g. ABC123"); + gameIdField.setMaxLength(MainMenuConfig.MAX_GAME_ID_LENGTH); + + joinConfirmBtn = new TextButton("Join", skin, "accent"); + TextButton backBtn = new TextButton("Back", skin, "default"); + + // Error label for inline validation messages + errorLabel = new Label("", skin, "small"); + errorLabel.setColor(com.badlogic.gdx.graphics.Color.RED); + + panel.add(hint).colspan(2).padBottom(8).row(); + panel.add(gameIdField) + .width(MainMenuConfig.GAME_ID_FIELD_WIDTH) + .height(MainMenuConfig.GAME_ID_FIELD_HEIGHT) + .padRight(8); + panel.add(joinConfirmBtn) + .width(MainMenuConfig.JOIN_BTN_WIDTH) + .height(MainMenuConfig.JOIN_BTN_HEIGHT) + .row(); + panel.add(backBtn) + .colspan(2) + .width(MainMenuConfig.BACK_BTN_WIDTH) + .height(MainMenuConfig.BACK_BTN_HEIGHT) + .padTop(8) + .row(); + panel.add(errorLabel) + .colspan(2) + .padTop(4) + .row(); + + // Button listeners + joinConfirmBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + onJoinPressed(); + } + }); + + backBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + onCancel(); + } + }); + + panel.setVisible(false); + } + + private void onJoinPressed() { + String gameId = gameIdField.getText().trim(); + if (gameId.isEmpty()) { + showError("Please enter a Game ID."); + return; + } + + joinConfirmBtn.setDisabled(true); + showError("Checking..."); + validator.validate(gameId); + } + + private void onCancel() { + validator.cancel(); + setVisible(false); + clearError(); + gameIdField.setText(""); + if (listener != null) { + listener.onCancel(); + } + } + + private LobbyValidator.ValidationListener createValidationListener() { + return new LobbyValidator.ValidationListener() { + @Override + public void onLobbyFound(Lobby lobby) { + joinConfirmBtn.setDisabled(false); + clearError(); + setVisible(false); + gameIdField.setText(""); + if (listener != null) { + listener.onJoin(lobby.getGameId()); + } + } + + @Override + public void onLobbyNotFound() { + joinConfirmBtn.setDisabled(false); + showError("Lobby not found. Check the Game ID."); + if (listener != null) { + listener.onError("Lobby not found"); + } + } + + @Override + public void onValidationError(String message) { + joinConfirmBtn.setDisabled(false); + showError(message); + if (listener != null) { + listener.onError(message); + } + } + }; + } + + public Table getPanel() { + return panel; + } + + public void setVisible(boolean visible) { + this.visible = visible; + panel.setVisible(visible); + if (!visible) { + validator.cancel(); + joinConfirmBtn.setDisabled(false); + clearError(); + } + } + + public boolean isVisible() { + return visible; + } + + public void showError(String message) { + errorLabel.setText(message); + } + + public void clearError() { + errorLabel.setText(""); + } + + public void setGameId(String gameId) { + gameIdField.setText(gameId); + } + + public String getGameId() { + return gameIdField.getText().trim(); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/LobbyValidator.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/LobbyValidator.java new file mode 100644 index 0000000..33d1d87 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/LobbyValidator.java @@ -0,0 +1,76 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/mainmenu/LobbyValidator.java +package com.group14.regicidechess.screens.mainmenu; + +import com.badlogic.gdx.Gdx; +import com.group14.regicidechess.database.DatabaseManager; +import com.group14.regicidechess.model.Lobby; + +/** + * Validates lobby existence in Firebase before navigating. + * Handles async validation and provides callbacks for success/failure. + */ +public class LobbyValidator { + + public interface ValidationListener { + void onLobbyFound(Lobby lobby); + void onLobbyNotFound(); + void onValidationError(String message); + } + + private final ValidationListener listener; + private boolean isValidating = false; + + public LobbyValidator(ValidationListener listener) { + this.listener = listener; + } + + /** + * Validates a Game ID by fetching it from Firebase. + * + * @param gameId the Game ID to validate + */ + public void validate(String gameId) { + if (isValidating) { + if (listener != null) { + listener.onValidationError("Already checking..."); + } + return; + } + + String trimmedId = gameId != null ? gameId.trim() : ""; + + if (trimmedId.isEmpty()) { + if (listener != null) { + listener.onValidationError("Please enter a Game ID."); + } + return; + } + + isValidating = true; + + DatabaseManager.getInstance().getApi().fetchLobby(trimmedId, + // Lobby found + lobby -> Gdx.app.postRunnable(() -> { + isValidating = false; + if (listener != null) { + listener.onLobbyFound(lobby); + } + }), + // Lobby not found + error -> Gdx.app.postRunnable(() -> { + isValidating = false; + if (listener != null) { + listener.onLobbyNotFound(); + } + }) + ); + } + + public boolean isValidating() { + return isValidating; + } + + public void cancel() { + isValidating = false; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuConfig.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuConfig.java new file mode 100644 index 0000000..b755beb --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuConfig.java @@ -0,0 +1,39 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuConfig.java +package com.group14.regicidechess.screens.mainmenu; + +/** + * Configuration constants for the main menu screen. + */ +public final class MainMenuConfig { + private MainMenuConfig() {} // Prevent instantiation + + // Viewport dimensions + public static final float VIEWPORT_WIDTH = 480f; + public static final float VIEWPORT_HEIGHT = 854f; + + // Layout dimensions + public static final int TITLE_PAD_TOP = 120; + public static final int TITLE_PAD_BOTTOM = 8; + public static final int SUBTITLE_PAD_BOTTOM = 80; + public static final int BUTTON_WIDTH = 300; + public static final int BUTTON_HEIGHT = 60; + public static final int BUTTON_PAD_BOTTOM = 20; + public static final int JOIN_BUTTON_PAD_BOTTOM = 16; + public static final int JOIN_PANEL_WIDTH = 320; + + // Join panel specific + public static final int GAME_ID_FIELD_WIDTH = 200; + public static final int GAME_ID_FIELD_HEIGHT = 50; + public static final int JOIN_BTN_WIDTH = 80; + public static final int JOIN_BTN_HEIGHT = 50; + public static final int BACK_BTN_WIDTH = 290; + public static final int BACK_BTN_HEIGHT = 48; + public static final int JOIN_PANEL_PAD = 16; + public static final int MAX_GAME_ID_LENGTH = 10; + + // Colors + public static final float BG_R = 0.12f; + public static final float BG_G = 0.12f; + public static final float BG_B = 0.15f; + public static final float BG_A = 1f; +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuScreen.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuScreen.java new file mode 100644 index 0000000..dbe5e68 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuScreen.java @@ -0,0 +1,186 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuScreen.java +package com.group14.regicidechess.screens.mainmenu; + +import com.badlogic.gdx.Game; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.utils.viewport.FitViewport; +import com.group14.regicidechess.input.ScreenInputHandler; +import com.group14.regicidechess.model.Lobby; +import com.group14.regicidechess.screens.lobby.LobbyMode; +import com.group14.regicidechess.screens.lobby.LobbyScreen; +import com.group14.regicidechess.states.MainMenuState; +import com.group14.regicidechess.utils.ResourceManager; + +/** + * MainMenuScreen — thin coordinator for the main menu. + * + * Refactored version with improved modularity: + * - UI extracted to MainMenuUI + * - Join panel logic extracted to JoinGamePanel + * - Lobby validation extracted to LobbyValidator + * - Screen focuses purely on coordination and navigation + */ +public class MainMenuScreen implements Screen, ScreenInputHandler.ScreenInputObserver { + + // LibGDX + private final Game game; + private final SpriteBatch batch; + private final Stage stage; + private final Skin skin; + private final ScreenInputHandler inputHandler; + + // State and UI + private final MainMenuState mainMenuState; + private final MainMenuUI mainMenuUI; + + public MainMenuScreen(Game game, SpriteBatch batch) { + this.game = game; + this.batch = batch; + + // Initialize state + this.mainMenuState = new MainMenuState(); + mainMenuState.enter(); + + // LibGDX setup + this.stage = new Stage(new FitViewport( + MainMenuConfig.VIEWPORT_WIDTH, + MainMenuConfig.VIEWPORT_HEIGHT), + batch); + this.skin = ResourceManager.getInstance().getSkin(); + this.inputHandler = new ScreenInputHandler(); + inputHandler.addObserver(this); + + // Build UI with listener + this.mainMenuUI = new MainMenuUI(skin, createUIListener()); + + // Add UI to stage + stage.addActor(mainMenuUI.build()); + } + + // ───────────────────────────────────────────────────────────────────────── + // Listeners + // ───────────────────────────────────────────────────────────────────────── + + private MainMenuUI.MainMenuUIListener createUIListener() { + return new MainMenuUI.MainMenuUIListener() { + @Override + public void onCreateLobby() { + // Navigate to lobby screen in HOST mode + game.setScreen(new LobbyScreen(game, batch, LobbyMode.HOST, null)); + } + + @Override + public void onJoinLobby(String gameId) { + // Fetch lobby data before navigating + // Note: JoinGamePanel already validated existence, + // but we still need the lobby data for board size and budget + fetchLobbyAndNavigate(gameId); + } + + @Override + public void onBack() { + // Not used in main menu + } + }; + } + + /** + * Fetches lobby data after validation and navigates to LobbyScreen. + * Since JoinGamePanel already validated that the lobby exists, + * this fetch should succeed. + */ + private void fetchLobbyAndNavigate(String gameId) { + mainMenuUI.showError("Loading..."); + + com.group14.regicidechess.database.DatabaseManager.getInstance() + .getApi() + .fetchLobby(gameId, + // Success - lobby found + lobby -> Gdx.app.postRunnable(() -> { + mainMenuUI.clearError(); + navigateToLobbyScreen(lobby); + }), + // Failure - should not happen since validation passed, but handle gracefully + error -> Gdx.app.postRunnable(() -> { + mainMenuUI.showError("Failed to load lobby data. Please try again."); + }) + ); + } + + private void navigateToLobbyScreen(Lobby lobby) { + game.setScreen(new LobbyScreen(game, batch, LobbyMode.JOIN, lobby)); + } + + // ───────────────────────────────────────────────────────────────────────── + // Screen lifecycle + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void show() { + Gdx.input.setInputProcessor( + new com.badlogic.gdx.InputMultiplexer(stage, inputHandler)); + } + + @Override + public void render(float delta) { + // Clear screen + Gdx.gl.glClearColor( + MainMenuConfig.BG_R, + MainMenuConfig.BG_G, + MainMenuConfig.BG_B, + MainMenuConfig.BG_A); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + + // Update state + mainMenuState.update(delta); + + // Draw UI + stage.act(delta); + stage.draw(); + } + + @Override + public void resize(int width, int height) { + stage.getViewport().update(width, height, true); + } + + @Override + public void pause() {} + + @Override + public void resume() {} + + @Override + public void hide() { + inputHandler.clearObservers(); + mainMenuState.exit(); + mainMenuUI.clearError(); + mainMenuUI.getJoinPanel().setVisible(false); + } + + @Override + public void dispose() { + stage.dispose(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Input handling (unused but required by interface) + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void onTap(int x, int y, int pointer, int button) {} + + @Override + public void onDrag(int x, int y, int pointer) {} + + @Override + public void onRelease(int x, int y, int pointer, int button) {} + + @Override + public void onKeyDown(int keycode) {} +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java new file mode 100644 index 0000000..4222be7 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java @@ -0,0 +1,163 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java +package com.group14.regicidechess.screens.mainmenu; + +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.utils.Align; + +/** + * Builds and manages the main menu UI. + * Contains title, subtitle, buttons, and the join panel. + */ +public class MainMenuUI { + + public interface MainMenuUIListener { + void onCreateLobby(); + void onJoinLobby(String gameId); + void onBack(); + } + + private final Skin skin; + private final MainMenuUIListener listener; + private final JoinGamePanel joinPanel; + + private Table root; + private TextButton createBtn; + private TextButton joinBtn; + private Label mainErrorLabel; + + public MainMenuUI(Skin skin, MainMenuUIListener listener) { + this.skin = skin; + this.listener = listener; + this.joinPanel = new JoinGamePanel(skin, createJoinPanelListener()); + } + + public Table build() { + root = new Table(); + root.setFillParent(true); + root.setBackground(skin.getDrawable("surface-pixel")); + + // Title + Label titleLabel = new Label("REGICIDE\nCHESS", skin, "title"); + titleLabel.setAlignment(Align.center); + + // Subtitle + Label subtitleLabel = new Label("online strategy chess", skin, "small"); + subtitleLabel.setAlignment(Align.center); + + // Buttons + createBtn = new TextButton("Create Lobby", skin, "accent"); + joinBtn = new TextButton("Join Lobby", skin, "default"); + createBtn.pad(12); + joinBtn.pad(12); + + // Main error label (for general errors) + mainErrorLabel = new Label("", skin, "small"); + mainErrorLabel.setColor(com.badlogic.gdx.graphics.Color.RED); + mainErrorLabel.setAlignment(Align.center); + + // Layout + root.add(titleLabel) + .expandX() + .padTop(MainMenuConfig.TITLE_PAD_TOP) + .padBottom(MainMenuConfig.TITLE_PAD_BOTTOM) + .row(); + root.add(subtitleLabel) + .expandX() + .padBottom(MainMenuConfig.SUBTITLE_PAD_BOTTOM) + .row(); + root.add(createBtn) + .width(MainMenuConfig.BUTTON_WIDTH) + .height(MainMenuConfig.BUTTON_HEIGHT) + .padBottom(MainMenuConfig.BUTTON_PAD_BOTTOM) + .row(); + root.add(joinBtn) + .width(MainMenuConfig.BUTTON_WIDTH) + .height(MainMenuConfig.BUTTON_HEIGHT) + .padBottom(MainMenuConfig.JOIN_BUTTON_PAD_BOTTOM) + .row(); + root.add(mainErrorLabel) + .expandX() + .padBottom(8) + .row(); + root.add(joinPanel.getPanel()) + .width(MainMenuConfig.JOIN_PANEL_WIDTH) + .row(); + + // Button listeners + createBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + onCreatePressed(); + } + }); + + joinBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + onJoinPressed(); + } + }); + + return root; + } + + private void onCreatePressed() { + clearError(); + if (listener != null) { + listener.onCreateLobby(); + } + } + + private void onJoinPressed() { + clearError(); + boolean isVisible = joinPanel.isVisible(); + joinPanel.setVisible(!isVisible); + if (isVisible) { + joinPanel.clearError(); + } + } + + private JoinGamePanel.JoinPanelListener createJoinPanelListener() { + return new JoinGamePanel.JoinPanelListener() { + @Override + public void onJoin(String gameId) { + if (listener != null) { + listener.onJoinLobby(gameId); + } + } + + @Override + public void onCancel() { + // No additional action needed + } + + @Override + public void onError(String message) { + // Show error in the main error label as well + showError(message); + } + }; + } + + public void showError(String message) { + mainErrorLabel.setText(message); + } + + public void clearError() { + mainErrorLabel.setText(""); + joinPanel.clearError(); + } + + public Table getRoot() { + return root; + } + + public JoinGamePanel getJoinPanel() { + return joinPanel; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/BoardCoordinateMapper.java b/core/src/main/java/com/group14/regicidechess/screens/setup/BoardCoordinateMapper.java new file mode 100644 index 0000000..e2cbb8a --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/BoardCoordinateMapper.java @@ -0,0 +1,43 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/BoardCoordinateMapper.java +package com.group14.regicidechess.screens.setup; + +import com.group14.regicidechess.model.Player; + +/** + * Handles conversion between logic coordinates and display coordinates. + * White: display = logic + * Black: display = (size - 1 - logic) + */ +public class BoardCoordinateMapper { + private final Player localPlayer; + private final int boardSize; + + public BoardCoordinateMapper(Player localPlayer, int boardSize) { + this.localPlayer = localPlayer; + this.boardSize = boardSize; + } + + public int toDisplayRow(int logicRow) { + return localPlayer.isWhite() ? logicRow : (boardSize - 1 - logicRow); + } + + public int toLogicRow(int displayRow) { + return localPlayer.isWhite() ? displayRow : (boardSize - 1 - displayRow); + } + + public int getHomeDisplayRowMin(int homeRowMin) { + return toDisplayRow(homeRowMin); + } + + public int getHomeDisplayRowMax(int homeRowMax) { + return toDisplayRow(homeRowMax); + } + + public Player getLocalPlayer() { + return localPlayer; + } + + public int getBoardSize() { + return boardSize; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/BoardFetchRetryPolicy.java b/core/src/main/java/com/group14/regicidechess/screens/setup/BoardFetchRetryPolicy.java new file mode 100644 index 0000000..2fa38be --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/BoardFetchRetryPolicy.java @@ -0,0 +1,45 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/BoardFetchRetryPolicy.java +package com.group14.regicidechess.screens.setup; + +import com.badlogic.gdx.Gdx; + +import java.util.Timer; +import java.util.TimerTask; + +/** + * Encapsulates retry logic for fetching opponent's board. + */ +public class BoardFetchRetryPolicy { + private final int maxRetries; + private final long retryDelayMs; + + public BoardFetchRetryPolicy() { + this(SetupScreenConfig.BOARD_FETCH_MAX_RETRIES, SetupScreenConfig.BOARD_FETCH_RETRY_MS); + } + + public BoardFetchRetryPolicy(int maxRetries, long retryDelayMs) { + this.maxRetries = maxRetries; + this.retryDelayMs = retryDelayMs; + } + + public boolean shouldRetry(int currentRetryCount) { + return currentRetryCount < maxRetries; + } + + public void scheduleRetry(Runnable retryAction, int retryNumber) { + new Timer().schedule(new TimerTask() { + @Override + public void run() { + Gdx.app.postRunnable(retryAction); + } + }, retryDelayMs); + } + + public int getMaxRetries() { + return maxRetries; + } + + public long getRetryDelayMs() { + return retryDelayMs; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardCodec.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardCodec.java new file mode 100644 index 0000000..2b54411 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardCodec.java @@ -0,0 +1,73 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardCodec.java +package com.group14.regicidechess.screens.setup; + +import com.group14.regicidechess.model.Board; +import com.group14.regicidechess.model.Player; +import com.group14.regicidechess.model.pieces.Bishop; +import com.group14.regicidechess.model.pieces.ChessPiece; +import com.group14.regicidechess.model.pieces.King; +import com.group14.regicidechess.model.pieces.Knight; +import com.group14.regicidechess.model.pieces.Pawn; +import com.group14.regicidechess.model.pieces.Queen; +import com.group14.regicidechess.model.pieces.Rook; + +/** + * SetupBoardCodec — converts between a Board's pieces and the int[][] format + * used by Firebase. + */ +public final class SetupBoardCodec { + + public static final int CODE_KING = 1; + public static final int CODE_QUEEN = 2; + public static final int CODE_ROOK = 3; + public static final int CODE_BISHOP = 4; + public static final int CODE_KNIGHT = 5; + public static final int CODE_PAWN = 6; + + private SetupBoardCodec() {} + + public static int[][] encode(Board board) { + int size = board.getSize(); + int[][] state = new int[size][size]; + for (ChessPiece piece : board.getPieces()) { + int col = (int) piece.getPosition().x; + int row = (int) piece.getPosition().y; + state[col][row] = toCode(piece); + } + return state; + } + + public static void mergeInto(int[][] encoded, Player owner, Board board) { + if (encoded == null || encoded.length == 0) return; + for (int col = 0; col < encoded.length; col++) { + for (int row = 0; row < encoded[col].length; row++) { + int code = encoded[col][row]; + if (code == 0) continue; + ChessPiece piece = decode(code, owner); + if (piece != null) board.placePiece(piece, col, row); + } + } + } + + public static int toCode(ChessPiece piece) { + if (piece instanceof King) return CODE_KING; + if (piece instanceof Queen) return CODE_QUEEN; + if (piece instanceof Rook) return CODE_ROOK; + if (piece instanceof Bishop) return CODE_BISHOP; + if (piece instanceof Knight) return CODE_KNIGHT; + if (piece instanceof Pawn) return CODE_PAWN; + return 0; + } + + public static ChessPiece decode(int code, Player owner) { + switch (code) { + case CODE_KING: return new King(owner); + case CODE_QUEEN: return new Queen(owner); + case CODE_ROOK: return new Rook(owner); + case CODE_BISHOP: return new Bishop(owner); + case CODE_KNIGHT: return new Knight(owner); + case CODE_PAWN: return new Pawn(owner); + default: return null; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardInputHandler.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardInputHandler.java new file mode 100644 index 0000000..bb8b67a --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardInputHandler.java @@ -0,0 +1,90 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardInputHandler.java +package com.group14.regicidechess.screens.setup; + +import com.group14.regicidechess.model.Player; +import com.group14.regicidechess.model.pieces.ChessPiece; +import com.group14.regicidechess.model.pieces.King; +import com.group14.regicidechess.states.SetupState; + +/** + * Handles board tap input for the setup screen. + */ +public class SetupBoardInputHandler { + + public interface BoardActionListener { + void onPiecePlaced(ChessPiece piece, int col, int row); + void onPieceRemoved(int col, int row); + void onInvalidPlacement(String reason); + void onStateChanged(); + } + + private final SetupState setupState; + private final SetupBoardRenderer renderer; + private final SetupPaletteWidget palette; + private final Player localPlayer; + private final BoardActionListener listener; + + public SetupBoardInputHandler(SetupState setupState, SetupBoardRenderer renderer, + SetupPaletteWidget palette, Player localPlayer, + BoardActionListener listener) { + this.setupState = setupState; + this.renderer = renderer; + this.palette = palette; + this.localPlayer = localPlayer; + this.listener = listener; + } + + public boolean handleTap(float worldX, float worldY) { + float cellSize = renderer.getCellSize(); + float boardLeft = renderer.getBoardLeft(); + float boardBottom = renderer.getBoardBottom(); + int size = setupState.getBoardSize(); + + if (worldX < boardLeft || worldX > boardLeft + size * cellSize) return false; + if (worldY < boardBottom || worldY > boardBottom + size * cellSize) return false; + + int col = Math.max(0, Math.min((int) ((worldX - boardLeft) / cellSize), size - 1)); + int dispRow = Math.max(0, Math.min((int) ((worldY - boardBottom) / cellSize), size - 1)); + int row = renderer.getCoordinateMapper().toLogicRow(dispRow); + + ChessPiece existing = setupState.getBoard().getPieceAt(col, row); + + if (existing != null) { + // Remove existing piece + setupState.removePiece(col, row); + listener.onPieceRemoved(col, row); + listener.onStateChanged(); + } else if (palette.hasSelection()) { + ChessPiece piece = palette.createSelectedPiece(localPlayer); + boolean success = setupState.placePiece(piece, col, row); + + if (success) { + // IMPORTANT: Do NOT clear selection - keep it so player can place more + // palette.afterPiecePlaced() is called but we don't clear selection + // The palette's keepSelectionAfterPlace flag is true by default + palette.afterPiecePlaced(); // This won't clear if keepSelectionAfterPlace is true + listener.onPiecePlaced(piece, col, row); + listener.onStateChanged(); + } else { + String reason = getPlacementFailureReason(piece); + listener.onInvalidPlacement(reason); + } + } + + return true; + } + + private String getPlacementFailureReason(ChessPiece piece) { + if (piece instanceof King && kingIsOnBoard()) { + return "You can only place one King!"; + } + return "Cannot place here — check home zone and budget."; + } + + private boolean kingIsOnBoard() { + for (ChessPiece p : setupState.getBoard().getPieces()) { + if (p instanceof King) return true; + } + return false; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardRenderer.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardRenderer.java new file mode 100644 index 0000000..787ab24 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardRenderer.java @@ -0,0 +1,150 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardRenderer.java +package com.group14.regicidechess.screens.setup; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.group14.regicidechess.model.pieces.ChessPiece; +import com.group14.regicidechess.states.SetupState; +import com.group14.regicidechess.utils.ResourceManager; + +/** + * SetupBoardRenderer — draws the setup-phase board and palette piece sprites. + */ +public class SetupBoardRenderer { + + private static final Color HOME_LIGHT = new Color(0.93f, 0.90f, 0.75f, 1f); + private static final Color HOME_DARK = new Color(0.45f, 0.62f, 0.32f, 1f); + private static final Color PLAIN_LIGHT = new Color(0.93f, 0.85f, 0.72f, 1f); + private static final Color PLAIN_DARK = new Color(0.55f, 0.38f, 0.24f, 1f); + private static final Color HOME_BORDER = new Color(0.4f, 0.85f, 0.4f, 0.9f); + private static final Color GRID_COLOR = new Color(0f, 0f, 0f, 0.3f); + + private final ShapeRenderer shapeRenderer; + private final SpriteBatch batch; + private final BoardCoordinateMapper coordinateMapper; + + private float boardLeft; + private float boardBottom; + private float cellSize; + + public SetupBoardRenderer(SpriteBatch batch, BoardCoordinateMapper coordinateMapper) { + this.batch = batch; + this.coordinateMapper = coordinateMapper; + this.shapeRenderer = new ShapeRenderer(); + } + + public void computeGeometry(float screenW, float screenH, + float headerH, float paletteH, float footerH, + int boardSize) { + float available = screenH - headerH - paletteH - footerH - 16f; + float maxW = screenW - 16f; + cellSize = Math.min(maxW / boardSize, available / boardSize); + boardLeft = (screenW - cellSize * boardSize) / 2f; + boardBottom = footerH + paletteH + (available - cellSize * boardSize) / 2f + 8f; + } + + public void drawBoard(Matrix4 projMatrix, SetupState state) { + int size = coordinateMapper.getBoardSize(); + shapeRenderer.setProjectionMatrix(projMatrix); + + drawTiles(state, size); + drawGrid(size); + drawHomeZoneBorder(state, size); + drawPlacedPieces(projMatrix, state, size); + } + + public void drawPaletteSprites(Matrix4 projMatrix, TextButton[] paletteButtons, String[] pieceNames) { + String color = coordinateMapper.getLocalPlayer().isWhite() ? "white" : "black"; + batch.setProjectionMatrix(projMatrix); + batch.begin(); + for (int i = 0; i < paletteButtons.length; i++) { + TextButton btn = paletteButtons[i]; + Texture tex = ResourceManager.getInstance().getPieceTexture(color, pieceNames[i].toLowerCase()); + float btnW = btn.getWidth(); + float btnH = btn.getHeight(); + Vector2 sp = btn.localToStageCoordinates(new Vector2(0, 0)); + float s = Math.min(btnW, btnH) * 0.42f; + batch.draw(tex, sp.x + (btnW - s) / 2f, sp.y + btnH - s - 4f, s, s); + } + batch.end(); + } + + private void drawTiles(SetupState state, int size) { + shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); + for (int col = 0; col < size; col++) { + for (int logicRow = 0; logicRow < size; logicRow++) { + int dispRow = coordinateMapper.toDisplayRow(logicRow); + float x = boardLeft + col * cellSize; + float y = boardBottom + dispRow * cellSize; + boolean light = (col + logicRow) % 2 == 0; + boolean home = logicRow >= state.getHomeRowMin() && logicRow <= state.getHomeRowMax(); + + if (home) { + shapeRenderer.setColor(light ? HOME_LIGHT : HOME_DARK); + } else { + shapeRenderer.setColor(light ? PLAIN_LIGHT : PLAIN_DARK); + } + shapeRenderer.rect(x, y, cellSize, cellSize); + } + } + shapeRenderer.end(); + } + + private void drawGrid(int size) { + shapeRenderer.begin(ShapeRenderer.ShapeType.Line); + shapeRenderer.setColor(GRID_COLOR); + for (int i = 0; i <= size; i++) { + float x = boardLeft + i * cellSize; + float y = boardBottom + i * cellSize; + shapeRenderer.line(x, boardBottom, x, boardBottom + size * cellSize); + shapeRenderer.line(boardLeft, y, boardLeft + size * cellSize, y); + } + shapeRenderer.end(); + } + + private void drawHomeZoneBorder(SetupState state, int size) { + shapeRenderer.begin(ShapeRenderer.ShapeType.Line); + shapeRenderer.setColor(HOME_BORDER); + Gdx.gl.glLineWidth(3f); + int homeDispMin = coordinateMapper.toDisplayRow(state.getHomeRowMin()); + int homeDispMax = coordinateMapper.toDisplayRow(state.getHomeRowMax()); + float zoneBottom = boardBottom + Math.min(homeDispMin, homeDispMax) * cellSize; + float zoneTop = boardBottom + (Math.max(homeDispMin, homeDispMax) + 1) * cellSize; + shapeRenderer.rect(boardLeft, zoneBottom, size * cellSize, zoneTop - zoneBottom); + Gdx.gl.glLineWidth(1f); + shapeRenderer.end(); + } + + private void drawPlacedPieces(Matrix4 projMatrix, SetupState state, int size) { + String color = coordinateMapper.getLocalPlayer().isWhite() ? "white" : "black"; + batch.setProjectionMatrix(projMatrix); + batch.begin(); + for (ChessPiece piece : state.getBoard().getPieces()) { + int col = (int) piece.getPosition().x; + int dispRow = coordinateMapper.toDisplayRow((int) piece.getPosition().y); + Texture tex = ResourceManager.getInstance().getPieceTexture(color, piece.getTypeName().toLowerCase()); + float pieceSize = cellSize * 0.8f; + float offset = (cellSize - pieceSize) / 2f; + batch.draw(tex, + boardLeft + col * cellSize + offset, + boardBottom + dispRow * cellSize + offset, + pieceSize, pieceSize); + } + batch.end(); + } + + public float getCellSize() { return cellSize; } + public float getBoardLeft() { return boardLeft; } + public float getBoardBottom() { return boardBottom; } + public BoardCoordinateMapper getCoordinateMapper() { return coordinateMapper; } + + public void dispose() { + shapeRenderer.dispose(); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java new file mode 100644 index 0000000..860bcfb --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java @@ -0,0 +1,109 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java +package com.group14.regicidechess.screens.setup; + +import com.badlogic.gdx.Gdx; +import com.group14.regicidechess.database.DatabaseManager; +import com.group14.regicidechess.model.Board; +import com.group14.regicidechess.model.Player; +import com.group14.regicidechess.states.SetupState; + +/** + * Manages the 3-step Firebase flow for setup confirmation. + */ +public class SetupFlowController { + + public interface FlowListener { + void onUploadComplete(); + void onBothReady(); + void onOpponentBoardFetched(Board finalBoard, Player opponent); + void onError(String message); + } + + private final String gameId; + private final Player localPlayer; + private final SetupState setupState; + private final FlowListener listener; + private final BoardFetchRetryPolicy retryPolicy; + + private int boardFetchRetries = 0; + + public SetupFlowController(String gameId, Player localPlayer, SetupState setupState, FlowListener listener) { + this(gameId, localPlayer, setupState, listener, new BoardFetchRetryPolicy()); + } + + public SetupFlowController(String gameId, Player localPlayer, SetupState setupState, + FlowListener listener, BoardFetchRetryPolicy retryPolicy) { + this.gameId = gameId; + this.localPlayer = localPlayer; + this.setupState = setupState; + this.listener = listener; + this.retryPolicy = retryPolicy; + } + + public void confirm() { + // Check UI readiness (King placed) + if (!setupState.isReadyForConfirm()) { + listener.onError("Place your King before confirming."); + return; + } + + // Now set the player as ready for Firebase + if (!setupState.setReady()) { + listener.onError("Failed to set ready state."); + return; + } + + listener.onUploadComplete(); + + int[][] encoded = SetupBoardCodec.encode(setupState.getBoard()); + DatabaseManager.getInstance().getApi().confirmSetup( + gameId, + localPlayer.isWhite(), + encoded, + this::listenForBothReady + ); + } + + private void listenForBothReady() { + DatabaseManager.getInstance().getApi().listenForBothSetupReady( + gameId, + () -> Gdx.app.postRunnable(() -> { + listener.onBothReady(); + fetchOpponentBoardWithRetry(); + }) + ); + } + + private void fetchOpponentBoardWithRetry() { + fetchOpponentBoard(); + } + + private void fetchOpponentBoard() { + DatabaseManager.getInstance().getApi().getOpponentBoard( + gameId, + localPlayer.isWhite(), + opponentBoard -> Gdx.app.postRunnable(() -> { + boolean empty = opponentBoard == null || opponentBoard.length == 0; + + if (empty && retryPolicy.shouldRetry(boardFetchRetries)) { + boardFetchRetries++; + retryPolicy.scheduleRetry(this::fetchOpponentBoard, boardFetchRetries); + return; + } + + if (empty) { + listener.onError("Failed to fetch opponent's board after multiple attempts."); + return; + } + + Player opponentPlayer = new Player( + localPlayer.isWhite() ? "player2" : "player1", + !localPlayer.isWhite(), + localPlayer.getBudget()); + SetupBoardCodec.mergeInto(opponentBoard, opponentPlayer, setupState.getBoard()); + + listener.onOpponentBoardFetched(setupState.getBoard(), opponentPlayer); + }) + ); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFooterWidget.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFooterWidget.java new file mode 100644 index 0000000..b46d36e --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFooterWidget.java @@ -0,0 +1,92 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupFooterWidget.java +package com.group14.regicidechess.screens.setup; + +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.utils.Align; + +/** + * SetupFooterWidget — builds and manages the footer UI. + */ +public class SetupFooterWidget { + + public interface FooterListener { + void onClear(); + void onConfirm(); + } + + private final Skin skin; + private final FooterListener listener; + + private TextButton confirmBtn; + private Label statusLabel; + private Label waitingLabel; + + public SetupFooterWidget(Skin skin, FooterListener listener) { + this.skin = skin; + this.listener = listener; + } + + public Table build() { + Table footer = new Table(); + footer.setBackground(skin.getDrawable("surface-pixel")); + footer.pad(10); + + TextButton clearBtn = new TextButton("Clear", skin, "danger"); + confirmBtn = new TextButton("Confirm", skin, "accent"); + confirmBtn.setDisabled(true); + + statusLabel = new Label("Place your King to continue", skin, "small"); + statusLabel.setAlignment(Align.center); + + waitingLabel = new Label("Waiting for opponent...", skin, "small"); + waitingLabel.setAlignment(Align.center); + waitingLabel.setVisible(false); + + clearBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + listener.onClear(); + } + }); + + confirmBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + if (!confirmBtn.isDisabled()) { + listener.onConfirm(); + } + } + }); + + footer.add(clearBtn).width(140).height(50).expandX().left(); + footer.add(statusLabel).expandX(); + footer.add(confirmBtn).width(140).height(50).expandX().right(); + + return footer; + } + + public void setConfirmEnabled(boolean enabled) { + confirmBtn.setDisabled(!enabled); + } + + public void setStatusMessage(String message) { + statusLabel.setText(message); + } + + public void setWaitingMode(boolean waiting) { + confirmBtn.setVisible(!waiting); + waitingLabel.setVisible(waiting); + if (waiting) { + setStatusMessage("Waiting for opponent to finish setup..."); + } + } + + public Label getWaitingLabel() { + return waitingLabel; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupHeaderWidget.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupHeaderWidget.java new file mode 100644 index 0000000..01b59fa --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupHeaderWidget.java @@ -0,0 +1,45 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupHeaderWidget.java +package com.group14.regicidechess.screens.setup; + +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.group14.regicidechess.model.Player; + +/** + * SetupHeaderWidget — builds and manages the header UI. + */ +public class SetupHeaderWidget { + private final Skin skin; + private final Player player; + private Label budgetLabel; + + public SetupHeaderWidget(Skin skin, Player player) { + this.skin = skin; + this.player = player; + } + + public Table build() { + Table header = new Table(); + header.setBackground(skin.getDrawable("primary-pixel")); + header.pad(12); + + Label titleLabel = new Label("SETUP", skin, "title"); + budgetLabel = new Label(getBudgetText(), skin); + + header.add(titleLabel).expandX().left(); + header.add(budgetLabel).expandX().right(); + + return header; + } + + public void refreshBudget() { + if (budgetLabel != null) { + budgetLabel.setText(getBudgetText()); + } + } + + private String getBudgetText() { + return "Budget: " + player.getBudgetRemaining() + " / " + player.getBudget(); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupPaletteWidget.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupPaletteWidget.java new file mode 100644 index 0000000..a32028c --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupPaletteWidget.java @@ -0,0 +1,150 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupPaletteWidget.java +package com.group14.regicidechess.screens.setup; + +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.utils.Align; +import com.group14.regicidechess.model.Player; +import com.group14.regicidechess.model.pieces.Bishop; +import com.group14.regicidechess.model.pieces.ChessPiece; +import com.group14.regicidechess.model.pieces.King; +import com.group14.regicidechess.model.pieces.Knight; +import com.group14.regicidechess.model.pieces.Pawn; +import com.group14.regicidechess.model.pieces.Queen; +import com.group14.regicidechess.model.pieces.Rook; + +/** + * SetupPaletteWidget — builds the scrollable piece-selection palette. + */ +public class SetupPaletteWidget { + + public static final String[] PIECE_NAMES = { + "King", "Queen", "Rook", "Bishop", "Knight", "Pawn" + }; + + public static final int[] PIECE_COSTS = { 0, 9, 5, 3, 3, 1 }; + + public interface Listener { + void onSelectionChanged(int selectedIndex); + } + + private final Skin skin; + private final Listener listener; + private final TextButton[] buttons; + private int selectedIndex = -1; + + // Option to keep selection after placing a piece + private boolean keepSelectionAfterPlace = true; + + public SetupPaletteWidget(Skin skin, Listener listener) { + this.skin = skin; + this.listener = listener; + this.buttons = new TextButton[PIECE_NAMES.length]; + } + + public SetupPaletteWidget(Skin skin, Listener listener, boolean keepSelectionAfterPlace) { + this(skin, listener); + this.keepSelectionAfterPlace = keepSelectionAfterPlace; + } + + public Table buildWidget() { + Table paletteWrapper = new Table(); + paletteWrapper.setBackground(skin.getDrawable("primary-dark-pixel")); + paletteWrapper.pad(8); + + Table palette = new Table(); + + for (int i = 0; i < PIECE_NAMES.length; i++) { + final int idx = i; + String cost = PIECE_COSTS[i] == 0 ? "free" : String.valueOf(PIECE_COSTS[i]); + String label = "\n\n" + PIECE_NAMES[i] + "\n[" + cost + "]"; + + TextButton btn = new TextButton(label, skin, "default"); + btn.getLabel().setAlignment(Align.center); + buttons[i] = btn; + + btn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + toggle(idx); + } + }); + palette.add(btn).width(68).height(90).pad(4); + } + + ScrollPane scroll = new ScrollPane(palette, skin); + scroll.setScrollingDisabled(false, true); + paletteWrapper.add(scroll).expandX().fillX().height(100); + return paletteWrapper; + } + + private void toggle(int idx) { + selectedIndex = (selectedIndex == idx) ? -1 : idx; + refreshButtonStyles(); + if (listener != null) { + listener.onSelectionChanged(selectedIndex); + } + } + + /** + * Clears the selection (use this when you want to force deselection). + */ + public void clearSelection() { + selectedIndex = -1; + refreshButtonStyles(); + } + + /** + * Called after a piece is placed. If keepSelectionAfterPlace is true, + * selection remains; otherwise it's cleared. + */ + public void afterPiecePlaced() { + if (!keepSelectionAfterPlace) { + clearSelection(); + } + } + + private void refreshButtonStyles() { + for (int i = 0; i < buttons.length; i++) { + buttons[i].setStyle(skin.get( + i == selectedIndex ? "accent" : "default", + TextButton.TextButtonStyle.class)); + } + } + + public ChessPiece createSelectedPiece(Player owner) { + return createPiece(selectedIndex, owner); + } + + public static ChessPiece createPiece(int idx, Player owner) { + switch (idx) { + case 0: return new King(owner); + case 1: return new Queen(owner); + case 2: return new Rook(owner); + case 3: return new Bishop(owner); + case 4: return new Knight(owner); + case 5: return new Pawn(owner); + default: return null; + } + } + + public TextButton[] getButtons() { + return buttons; + } + + public int getSelectedIndex() { + return selectedIndex; + } + + public boolean hasSelection() { + return selectedIndex >= 0; + } + + public void setKeepSelectionAfterPlace(boolean keep) { + this.keepSelectionAfterPlace = keep; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java new file mode 100644 index 0000000..3ec4893 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java @@ -0,0 +1,360 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java +package com.group14.regicidechess.screens.setup; + +import com.badlogic.gdx.Game; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.utils.viewport.FitViewport; +import com.group14.regicidechess.input.ScreenInputHandler; +import com.group14.regicidechess.model.Player; +import com.group14.regicidechess.model.pieces.ChessPiece; +import com.group14.regicidechess.model.pieces.King; +import com.group14.regicidechess.screens.game.GameScreen; +import com.group14.regicidechess.states.SetupState; +import com.group14.regicidechess.utils.ResourceManager; + +/** + * SetupScreen — thin coordinator for the piece-placement phase. + * + * Refactored version with improved modularity: + * - UI components extracted to separate widgets + * - Firebase flow extracted to SetupFlowController + * - Board input logic extracted to SetupBoardInputHandler + * - Coordinate mapping extracted to BoardCoordinateMapper + * - Palette keeps selection after placing pieces for faster placement + */ +public class SetupScreen implements Screen, ScreenInputHandler.ScreenInputObserver { + + // LibGDX + private final Game game; + private final SpriteBatch batch; + private final Stage stage; + private final Skin skin; + private final ScreenInputHandler inputHandler; + + // Game state + private final SetupState setupState; + private final Player localPlayer; + private final String gameId; + @SuppressWarnings("unused") + private final boolean isHost; + + // UI Widgets + private final SetupHeaderWidget headerWidget; + private final SetupFooterWidget footerWidget; + private final SetupPaletteWidget palette; + + // Helpers + private final SetupBoardRenderer boardRenderer; + private final SetupBoardInputHandler boardInputHandler; + private final SetupFlowController flowController; + + @SuppressWarnings("unused") + private Table root; + + public SetupScreen(Game game, SpriteBatch batch, + String gameId, int boardSize, int budget, boolean isHost) { + this.game = game; + this.batch = batch; + this.gameId = gameId; + this.isHost = isHost; + + // Host = white (player1), joiner = black (player2) + this.localPlayer = new Player(isHost ? "player1" : "player2", isHost, budget); + + // Setup state + this.setupState = new SetupState(); + setupState.setBoardSize(boardSize); + setupState.setBudget(budget); + setupState.setPlayer(localPlayer); + setupState.enter(); + + // LibGDX setup + this.stage = new Stage(new FitViewport( + SetupScreenConfig.VIEWPORT_WIDTH, + SetupScreenConfig.VIEWPORT_HEIGHT), + batch); + this.skin = ResourceManager.getInstance().getSkin(); + this.inputHandler = new ScreenInputHandler(); + inputHandler.addObserver(this); + + // Create widgets and helpers + // IMPORTANT: Pass true to keep selection after placing pieces + this.palette = new SetupPaletteWidget(skin, this::onPaletteSelectionChanged, true); + + BoardCoordinateMapper coordinateMapper = new BoardCoordinateMapper(localPlayer, boardSize); + + this.boardRenderer = new SetupBoardRenderer(batch, coordinateMapper); + this.headerWidget = new SetupHeaderWidget(skin, localPlayer); + this.footerWidget = new SetupFooterWidget(skin, createFooterListener()); + + this.boardInputHandler = new SetupBoardInputHandler( + setupState, boardRenderer, palette, localPlayer, createBoardActionListener()); + + this.flowController = new SetupFlowController( + gameId, localPlayer, setupState, createFlowListener()); + + buildUI(); + boardRenderer.computeGeometry( + SetupScreenConfig.VIEWPORT_WIDTH, + SetupScreenConfig.VIEWPORT_HEIGHT, + SetupScreenConfig.HEADER_HEIGHT, + SetupScreenConfig.PALETTE_HEIGHT, + SetupScreenConfig.FOOTER_HEIGHT, + boardSize); + + // Log initial state for debugging + Gdx.app.log("SetupScreen", "Initialized with board size: " + boardSize + ", budget: " + budget); + } + + // ───────────────────────────────────────────────────────────────────────── + // UI construction + // ───────────────────────────────────────────────────────────────────────── + + private void buildUI() { + root = new Table(); + root.setFillParent(true); + root.top(); + stage.addActor(root); + + // Header + root.add(headerWidget.build()) + .expandX().fillX() + .height(SetupScreenConfig.HEADER_HEIGHT) + .row(); + + // Spacer for board area (drawn by boardRenderer) + root.add().expandX().expandY().row(); + + // Palette + root.add(palette.buildWidget()) + .expandX().fillX() + .height(SetupScreenConfig.PALETTE_HEIGHT) + .row(); + + // Footer + root.add(footerWidget.build()) + .expandX().fillX() + .height(SetupScreenConfig.FOOTER_HEIGHT) + .row(); + + // Waiting label (initially hidden) + root.add(footerWidget.getWaitingLabel()) + .expandX().padTop(8).row(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Listeners + // ───────────────────────────────────────────────────────────────────────── + + private SetupFooterWidget.FooterListener createFooterListener() { + return new SetupFooterWidget.FooterListener() { + @Override + public void onClear() { + Gdx.app.log("SetupScreen", "Clear button pressed"); + setupState.clearBoard(); + palette.clearSelection(); + refreshUI(); + } + + @Override + public void onConfirm() { + Gdx.app.log("SetupScreen", "Confirm button pressed"); + flowController.confirm(); + } + }; + } + + private SetupBoardInputHandler.BoardActionListener createBoardActionListener() { + return new SetupBoardInputHandler.BoardActionListener() { + @Override + public void onPiecePlaced(ChessPiece piece, int col, int row) { + Gdx.app.log("SetupScreen", "Piece placed: " + piece.getTypeName() + " at (" + col + ", " + row + ")"); + refreshUI(); + } + + @Override + public void onPieceRemoved(int col, int row) { + Gdx.app.log("SetupScreen", "Piece removed at (" + col + ", " + row + ")"); + refreshUI(); + } + + @Override + public void onInvalidPlacement(String reason) { + Gdx.app.log("SetupScreen", "Invalid placement: " + reason); + showStatus(reason); + } + + @Override + public void onStateChanged() { + refreshUI(); + } + }; + } + + private SetupFlowController.FlowListener createFlowListener() { + return new SetupFlowController.FlowListener() { + @Override + public void onUploadComplete() { + Gdx.app.postRunnable(() -> { + Gdx.app.log("SetupScreen", "Upload complete, waiting for opponent"); + footerWidget.setConfirmEnabled(false); + footerWidget.setWaitingMode(true); + }); + } + + @Override + public void onBothReady() { + Gdx.app.log("SetupScreen", "Both players ready"); + // Nothing to do here, waiting for board fetch + } + + @Override + public void onOpponentBoardFetched(com.group14.regicidechess.model.Board finalBoard, + Player opponent) { + Gdx.app.postRunnable(() -> { + Gdx.app.log("SetupScreen", "Opponent board fetched, navigating to GameScreen"); + game.setScreen(new GameScreen( + game, batch, + finalBoard, + localPlayer, + setupState.getBoardSize(), + gameId)); + }); + } + + @Override + public void onError(String message) { + Gdx.app.postRunnable(() -> { + Gdx.app.log("SetupScreen", "Error: " + message); + showStatus(message); + footerWidget.setConfirmEnabled(true); + footerWidget.setWaitingMode(false); + }); + } + }; + } + + private void onPaletteSelectionChanged(int selectedIndex) { + if (selectedIndex >= 0) { + Gdx.app.log("SetupScreen", "Selected piece: " + SetupPaletteWidget.PIECE_NAMES[selectedIndex]); + } + // Widget handles its own button styles, no action needed + } + + // ───────────────────────────────────────────────────────────────────────── + // Rendering + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void render(float delta) { + Gdx.gl.glClearColor(0.12f, 0.12f, 0.15f, 1f); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + setupState.update(delta); + + // Board is drawn before the stage so pieces sit below Scene2D actors + boardRenderer.drawBoard(stage.getCamera().combined, setupState); + stage.act(delta); + stage.draw(); + + // Palette sprites are drawn after the stage so they appear on top of buttons + boardRenderer.drawPaletteSprites(stage.getCamera().combined, + palette.getButtons(), SetupPaletteWidget.PIECE_NAMES); + } + + // ───────────────────────────────────────────────────────────────────────── + // Input + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void onTap(int screenX, int screenY, int pointer, int button) { + Vector2 world = stage.getViewport().unproject(new Vector2(screenX, screenY)); + boardInputHandler.handleTap(world.x, world.y); + } + + @Override + public void onDrag(int x, int y, int pointer) {} + + @Override + public void onRelease(int x, int y, int pointer, int button) {} + + @Override + public void onKeyDown(int keycode) {} + + // ───────────────────────────────────────────────────────────────────────── + // UI refresh + // ───────────────────────────────────────────────────────────────────────── + + private void refreshUI() { + headerWidget.refreshBudget(); + + // Use the correct method for UI readiness + boolean kingPlaced = setupState.isReadyForConfirm(); + footerWidget.setConfirmEnabled(kingPlaced); + + // Debug logging + int pieceCount = setupState.getBoard().getPieces().size(); + int playerPieces = setupState.getBoard().getPieces(setupState.getPlayer()).size(); + Gdx.app.log("SetupScreen", "Refresh UI - King placed: " + kingPlaced + + ", Total pieces: " + pieceCount + + ", Player pieces: " + playerPieces + + ", Budget remaining: " + localPlayer.getBudgetRemaining()); + + showStatus(kingPlaced + ? "Ready! Press Confirm when done." + : "Place your King to continue."); + } + + private void showStatus(String msg) { + footerWidget.setStatusMessage(msg); + } + + // ───────────────────────────────────────────────────────────────────────── + // Screen lifecycle + // ───────────────────────────────────────────────────────────────────────── + + @Override + public void show() { + Gdx.app.log("SetupScreen", "Screen shown"); + Gdx.input.setInputProcessor( + new com.badlogic.gdx.InputMultiplexer(stage, inputHandler)); + } + + @Override + public void resize(int width, int height) { + stage.getViewport().update(width, height, true); + boardRenderer.computeGeometry( + SetupScreenConfig.VIEWPORT_WIDTH, + SetupScreenConfig.VIEWPORT_HEIGHT, + SetupScreenConfig.HEADER_HEIGHT, + SetupScreenConfig.PALETTE_HEIGHT, + SetupScreenConfig.FOOTER_HEIGHT, + setupState.getBoardSize()); + } + + @Override + public void pause() {} + + @Override + public void resume() {} + + @Override + public void hide() { + Gdx.app.log("SetupScreen", "Screen hidden"); + inputHandler.clearObservers(); + setupState.exit(); + } + + @Override + public void dispose() { + Gdx.app.log("SetupScreen", "Screen disposed"); + stage.dispose(); + boardRenderer.dispose(); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreenConfig.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreenConfig.java new file mode 100644 index 0000000..16fc784 --- /dev/null +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreenConfig.java @@ -0,0 +1,22 @@ +// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreenConfig.java +package com.group14.regicidechess.screens.setup; + +/** + * Configuration constants for the setup screen. + */ +public final class SetupScreenConfig { + private SetupScreenConfig() {} // Prevent instantiation + + // Viewport dimensions + public static final float VIEWPORT_WIDTH = 480f; + public static final float VIEWPORT_HEIGHT = 854f; + + // Layout heights + public static final float HEADER_HEIGHT = 80f; + public static final float PALETTE_HEIGHT = 130f; + public static final float FOOTER_HEIGHT = 70f; + + // Board fetch retry configuration + public static final int BOARD_FETCH_MAX_RETRIES = 5; + public static final long BOARD_FETCH_RETRY_MS = 600L; +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/states/LobbyState.java b/core/src/main/java/com/group14/regicidechess/states/LobbyState.java index d56ea7e..660e7d5 100644 --- a/core/src/main/java/com/group14/regicidechess/states/LobbyState.java +++ b/core/src/main/java/com/group14/regicidechess/states/LobbyState.java @@ -1,9 +1,9 @@ package com.group14.regicidechess.states; +import com.group14.regicidechess.database.DatabaseManager; import com.group14.regicidechess.model.Lobby; import com.group14.regicidechess.model.Player; -/** Placement: core/src/main/java/com/group14/regicidechess/states/LobbyState.java */ public class LobbyState extends GameState { private Lobby lobby; @@ -14,19 +14,58 @@ public class LobbyState extends GameState { @Override public void update(float delta) {} @Override public void exit() {} + /** + * Creates a lobby locally and persists it to Firebase. + * onSuccess is called on the LibGDX thread with the generated Game ID. + */ + public void createLobby(int boardSize, int budget, + Runnable onSuccess, Runnable onError) { + lobby = new Lobby(boardSize, budget); + playerOne = new Player("player1", true, budget); + + DatabaseManager.getInstance().getApi().createLobby(lobby, + gameId -> com.badlogic.gdx.Gdx.app.postRunnable(onSuccess), + err -> { + com.badlogic.gdx.Gdx.app.log("LobbyState", "createLobby error: " + err); + com.badlogic.gdx.Gdx.app.postRunnable(onError); + }); + } + + /** + * Fetches an existing lobby from Firebase by Game ID. + * onSuccess is called with the populated Lobby on the LibGDX thread. + */ + public void joinLobby(String gameId, + Runnable onSuccess, Runnable onError) { + DatabaseManager.getInstance().getApi().joinLobby(gameId, + fetchedLobby -> com.badlogic.gdx.Gdx.app.postRunnable(() -> { + lobby = fetchedLobby; + playerTwo = new Player("player2", false, fetchedLobby.getBudget()); + onSuccess.run(); + }), + err -> { + com.badlogic.gdx.Gdx.app.log("LobbyState", "joinLobby error: " + err); + com.badlogic.gdx.Gdx.app.postRunnable(onError); + }); + } + + // Keep old stub method name for backwards compatibility (local-only testing) public String generateGameId(int boardSize, int budget) { lobby = new Lobby(boardSize, budget); playerOne = new Player("player1", true, budget); return lobby.getGameId(); } - public void joinByGameId(String id) { - // TODO: lobby = DatabaseManager.getInstance().fetchLobby(id); - // playerTwo = new Player("player2", false, lobby.getBudget()); + /** Called when MainMenuScreen has already fetched the lobby. */ + public void setPrefetchedLobby(Lobby prefetched) { + this.lobby = prefetched; + if (prefetched != null) { + playerTwo = new Player("player2", false, prefetched.getBudget()); + } } - public Lobby getLobby() { return lobby; } - public Player getPlayerOne() { return playerOne; } - public Player getPlayerTwo() { return playerTwo; } - public void setPlayerTwo(Player p) { this.playerTwo = p; } + public Lobby getLobby() { return lobby; } + public Player getPlayerOne() { return playerOne; } + public Player getPlayerTwo() { return playerTwo; } + public void setPlayerTwo(Player p) { this.playerTwo = p; } } \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/states/SetupState.java b/core/src/main/java/com/group14/regicidechess/states/SetupState.java index 5a2c060..46485ea 100644 --- a/core/src/main/java/com/group14/regicidechess/states/SetupState.java +++ b/core/src/main/java/com/group14/regicidechess/states/SetupState.java @@ -1,10 +1,17 @@ +// File: core/src/main/java/com/group14/regicidechess/states/SetupState.java package com.group14.regicidechess.states; + import com.group14.regicidechess.model.Board; import com.group14.regicidechess.model.Player; import com.group14.regicidechess.model.pieces.ChessPiece; import com.group14.regicidechess.model.pieces.King; -/** Placement: core/src/main/java/com/group14/regicidechess/states/SetupState.java */ +/** + * SetupState — manages the piece placement phase. + * + * Important: isReady() returns true if King is placed, NOT player.isReady(). + * The player.isReady() flag is used for the Firebase flow, not for UI readiness. + */ public class SetupState extends GameState { private Board board; @@ -14,21 +21,33 @@ public class SetupState extends GameState { private int homeRowMin; private int homeRowMax; - @Override public void enter() {} - @Override public void update(float delta) {} - @Override public void exit() {} + @Override + public void enter() { + com.badlogic.gdx.Gdx.app.log("SetupState", "Enter"); + } + + @Override + public void update(float delta) {} + + @Override + public void exit() { + com.badlogic.gdx.Gdx.app.log("SetupState", "Exit"); + } public void setBoardSize(int size) { this.boardSize = size; this.board = new Board(size); + com.badlogic.gdx.Gdx.app.log("SetupState", "Board size set to: " + size); } public void setBudget(int budget) { this.budget = budget; if (player != null) player.resetBudget(); + com.badlogic.gdx.Gdx.app.log("SetupState", "Budget set to: " + budget); } private static final int HOME_ROWS = 2; + public void setPlayer(Player player) { this.player = player; if (player.isWhite()) { @@ -38,39 +57,90 @@ public void setPlayer(Player player) { homeRowMin = boardSize - HOME_ROWS; // rows (size-2)–(size-1) homeRowMax = boardSize - 1; } + com.badlogic.gdx.Gdx.app.log("SetupState", "Player set: " + player.getPlayerId() + + ", home rows: " + homeRowMin + "-" + homeRowMax); } public boolean placePiece(ChessPiece piece, int col, int row) { - if (!isInHomeZone(row)) return false; + if (!isInHomeZone(row)) { + com.badlogic.gdx.Gdx.app.log("SetupState", "Placement failed: not in home zone (row " + row + ")"); + return false; + } - if (piece instanceof King && kingIsPlaced()) return false; + if (piece instanceof King && kingIsPlaced()) { + com.badlogic.gdx.Gdx.app.log("SetupState", "Placement failed: King already placed"); + return false; + } - if (!player.spendBudget(piece.getPointCost())) return false; + if (!player.spendBudget(piece.getPointCost())) { + com.badlogic.gdx.Gdx.app.log("SetupState", "Placement failed: insufficient budget"); + return false; + } + ChessPiece existing = board.getPieceAt(col, row); - if (existing != null) player.refundBudget(existing.getPointCost()); + if (existing != null) { + player.refundBudget(existing.getPointCost()); + } + board.placePiece(piece, col, row); + com.badlogic.gdx.Gdx.app.log("SetupState", "Piece placed: " + piece.getTypeName() + + " at (" + col + ", " + row + "), budget remaining: " + + player.getBudgetRemaining()); return true; } public void removePiece(int col, int row) { ChessPiece removed = board.removePiece(col, row); - if (removed != null) player.refundBudget(removed.getPointCost()); + if (removed != null) { + player.refundBudget(removed.getPointCost()); + com.badlogic.gdx.Gdx.app.log("SetupState", "Piece removed: " + removed.getTypeName() + + " at (" + col + ", " + row + "), budget remaining: " + + player.getBudgetRemaining()); + } } public void clearBoard() { board.clear(); player.resetBudget(); + com.badlogic.gdx.Gdx.app.log("SetupState", "Board cleared, budget reset"); } + /** + * Sets the player as ready for Firebase flow. + * This should ONLY be called when confirming the setup. + */ public boolean setReady() { - if (!kingIsPlaced()) return false; + if (!kingIsPlaced()) { + com.badlogic.gdx.Gdx.app.log("SetupState", "setReady() failed: King not placed"); + return false; + } player.setReady(); + com.badlogic.gdx.Gdx.app.log("SetupState", "setReady() success, player ready flag set"); return true; } + /** + * Checks if the player is ready for the Firebase flow. + * This is different from UI readiness (which only requires King). + */ + public boolean isPlayerReady() { + return player != null && player.isReady(); + } + + /** + * UI readiness: King must be placed. + * This is what the confirm button should check. + */ + public boolean isReadyForConfirm() { + return kingIsPlaced(); + } + private boolean kingIsPlaced() { - for (ChessPiece p : board.getPieces(player)) - if (p instanceof King) return true; + for (ChessPiece p : board.getPieces(player)) { + if (p instanceof King) { + return true; + } + } return false; } @@ -84,5 +154,12 @@ private boolean isInHomeZone(int row) { public int getBudget() { return budget; } public int getHomeRowMin() { return homeRowMin; } public int getHomeRowMax() { return homeRowMax; } - public boolean isReady() { return player != null && player.isReady(); } + + /** + * @deprecated Use isReadyForConfirm() for UI readiness, or isPlayerReady() for Firebase readiness. + */ + @Deprecated + public boolean isReady() { + return isReadyForConfirm(); + } } \ No newline at end of file