diff --git a/app/src/main/java/common/ConsumerSignal.java b/app/src/main/java/common/ConsumerSignal.java new file mode 100644 index 0000000..ba4b046 --- /dev/null +++ b/app/src/main/java/common/ConsumerSignal.java @@ -0,0 +1,27 @@ +package common; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +public class ConsumerSignal { + private final Set> listeners; + + public ConsumerSignal() { + this.listeners = new HashSet<>(); + } + + public void connect(Consumer listener) { + this.listeners.add(listener); + } + + public void clear() { + this.listeners.clear(); + } + + public void emit(T arg) { + for (Consumer listener : this.listeners) { + listener.accept(arg); + } + } +} diff --git a/app/src/main/java/game/Game.java b/app/src/main/java/game/Game.java index 4daf004..6e5d502 100644 --- a/app/src/main/java/game/Game.java +++ b/app/src/main/java/game/Game.java @@ -1,6 +1,10 @@ package game; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import sudoku.structure.MultiDoku; @@ -8,15 +12,19 @@ import sudoku.structure.MultiDoku; public class Game { public static enum GameState { - GameNotStarted, GameGoing, GameEnd + GameNotStarted, GameGoing } private final Map players; + private final List leaderboard; private GameState gameState; private MultiDoku doku; + private Instant startTime = null; + private long gameDuration; public Game() { this.players = new HashMap<>(); + this.leaderboard = new ArrayList<>(); this.gameState = GameState.GameNotStarted; } @@ -26,19 +34,33 @@ public class Game { public void addPlayer(Player player) { players.put(player.getId(), player); + leaderboard.add(player); + } + + public void setPlayerRemainingCells(Player player, int newScore) { + player.setRemainingCells(newScore); + Collections.sort(this.leaderboard, + (player1, player2) -> Integer.compare(player1.getRemainingCells(), player2.getRemainingCells())); } public void removePlayer(int id) { - players.remove(id); + this.leaderboard.remove(getPlayerById(id)); + this.players.remove(id); } public Map getPlayers() { return players; } - public void startGame(MultiDoku doku) { + public void startGame(MultiDoku doku, Instant startTime, long gameDuration) { this.doku = doku; this.gameState = GameState.GameGoing; + this.startTime = startTime; + this.gameDuration = gameDuration; + } + + public void stopGame() { + this.gameState = GameState.GameNotStarted; } public GameState getGameState() { @@ -49,4 +71,16 @@ public class Game { return doku; } + public List getLeaderboard() { + return leaderboard; + } + + public Instant getStartTime() { + return startTime; + } + + public long getGameDuration() { + return gameDuration; + } + } diff --git a/app/src/main/java/game/Player.java b/app/src/main/java/game/Player.java index 2336152..0b40f34 100644 --- a/app/src/main/java/game/Player.java +++ b/app/src/main/java/game/Player.java @@ -8,10 +8,20 @@ public class Player implements Serializable { private final String pseudo; private final int id; + private int score; public Player(int id, String pseudo) { this.pseudo = pseudo; this.id = id; + this.score = 0; + } + + public int getRemainingCells() { + return score; + } + + void setRemainingCells(int score) { + this.score = score; } public String getPseudo() { diff --git a/app/src/main/java/gui/menu/EndGameView.java b/app/src/main/java/gui/menu/EndGameView.java new file mode 100644 index 0000000..409c3f1 --- /dev/null +++ b/app/src/main/java/gui/menu/EndGameView.java @@ -0,0 +1,49 @@ +package gui.menu; + +import game.Player; +import gui.ColorGenerator; +import gui.widget.SudokuRenderer; +import imgui.ImGui; +import imgui.ImVec4; +import sudoku.structure.MultiDoku; + +public class EndGameView extends BaseView { + + private final Player winner; + private float time = 0; + + private static final ImVec4 YELLOW = new ImVec4(1, 1, 0, 1); + + private final SudokuRenderer sudokuRenderer; + + public EndGameView(StateMachine stateMachine, MultiDoku resolved, Player winner) { + super(stateMachine); + this.winner = winner; + this.sudokuRenderer = new SudokuRenderer(resolved); + } + + private ImVec4 getPseudoColor() { + time += ImGui.getIO().getDeltaTime(); + float factor = (float) Math.cos(time); + var color = ColorGenerator.hslToRgb(factor * factor, 0.9f, 0.4f); + return new ImVec4(color.r, color.g, color.b, 1.0f); + } + + private void renderWinText() { + String winText = " a gagné !"; + String text = winner.getPseudo() + winText; + float textWidth = ImGui.calcTextSizeX(text); + ImGui.setCursorPosX(ImGui.getIO().getDisplaySizeX() / 2.0f - textWidth / 2.0f); + ImGui.textColored(getPseudoColor(), winner.getPseudo()); + ImGui.sameLine(); + ImGui.textColored(YELLOW, winText); + } + + @Override + public void render() { + renderWinText(); + this.sudokuRenderer.render(); + renderReturnButton(); + } + +} diff --git a/app/src/main/java/gui/menu/MultiPlayerDokuView.java b/app/src/main/java/gui/menu/MultiPlayerDokuView.java index 8974cd0..f9ad32a 100644 --- a/app/src/main/java/gui/menu/MultiPlayerDokuView.java +++ b/app/src/main/java/gui/menu/MultiPlayerDokuView.java @@ -1,22 +1,50 @@ package gui.menu; -import gui.SudokuRenderer; +import game.Player; +import gui.widget.LeaderboardRenderer; +import gui.widget.MultiPlayerCompleteProgress; +import gui.widget.SudokuRenderer; +import gui.widget.TimerRenderer; import imgui.ImGui; import network.client.Client; import network.server.Server; +import sudoku.solver.BacktrackingSolver; +import sudoku.solver.Solver; +import sudoku.structure.Cell; +import sudoku.structure.MultiDoku; -public class MultiPlayerDokuView extends BaseView{ +public class MultiPlayerDokuView extends BaseView { private final Client client; private final Server server; private final SudokuRenderer sudokuRenderer; + private final LeaderboardRenderer leaderboardRenderer; + private final TimerRenderer timerRenderer; + private final MultiPlayerCompleteProgress completeProgress; public MultiPlayerDokuView(StateMachine stateMachine, Client client, Server server) { super(stateMachine); this.client = client; this.server = server; this.sudokuRenderer = new SudokuRenderer(this.client.getGame().getDoku()); + this.leaderboardRenderer = new LeaderboardRenderer(client.getGame(), client.getPlayer()); + this.sudokuRenderer.onCellChange.connect(this::onCellChange); this.client.onDisconnect.connect(this::onDisconnect); + this.client.onGameEnd.connect(this::onGameEnd); + this.timerRenderer = new TimerRenderer(this.client.getGame().getStartTime(), this.client.getGame().getGameDuration()); + this.completeProgress = new MultiPlayerCompleteProgress(this.client.getGame()); + } + + private void onGameEnd(Player winner) { + MultiDoku doku = this.client.getGame().getDoku(); + doku.clearMutableCells(); + Solver solver = new BacktrackingSolver(); + solver.solve(doku); + this.stateMachine.overrideState(new EndGameView(stateMachine, doku, winner)); + } + + private void onCellChange(Cell cell) { + this.client.sendCellChange(cell); } public void onDisconnect() { @@ -27,6 +55,9 @@ public class MultiPlayerDokuView extends BaseView{ @Override public void render() { + this.timerRenderer.render(); + this.leaderboardRenderer.render(); + this.completeProgress.render(); this.sudokuRenderer.render(); if (ImGui.button("Quitter")) { this.client.stop(); diff --git a/app/src/main/java/gui/menu/MultiPlayerView.java b/app/src/main/java/gui/menu/MultiPlayerView.java index e58f24d..c5668ad 100644 --- a/app/src/main/java/gui/menu/MultiPlayerView.java +++ b/app/src/main/java/gui/menu/MultiPlayerView.java @@ -1,24 +1,30 @@ package gui.menu; -import java.util.Arrays; - import game.Player; +import gui.widget.SudokuSelector; import imgui.ImGui; +import imgui.type.ImInt; import network.client.Client; import network.server.Server; -import sudoku.constraint.Constraint; import sudoku.structure.MultiDoku; -import sudoku.structure.SudokuFactory; public class MultiPlayerView extends BaseView { private final Client client; private final Server server; + private final SudokuSelector selector; + + private ImInt gameDurationMinutes = new ImInt(10); + + private MultiDoku doku = null; + public MultiPlayerView(StateMachine stateMachine, Client client, Server server) { super(stateMachine); this.client = client; this.server = server; + this.selector = new SudokuSelector(false, "Sélectionner le sudoku"); + this.selector.onSelect.connect(this::onSelected); this.client.onDisconnect.connect(this::onDisconnect); this.client.onGameStarted .connect(() -> this.stateMachine.pushState(new MultiPlayerDokuView(stateMachine, client, server))); @@ -34,26 +40,40 @@ public class MultiPlayerView extends BaseView { this.stateMachine.popState(); } + private void onSelected(MultiDoku doku) { + this.doku = doku; + } + public void renderGameStatus() { if (this.server == null) { ImGui.text("En attente de l'administrateur du serveur ..."); } else { + renderTimer(); + ImGui.beginDisabled(this.doku == null); if (ImGui.button("Démarrer")) { - // temp - MultiDoku doku = SudokuFactory.createBasicXShapedMultidoku(3, Arrays.asList(Constraint.Diagonal)); - this.server.startGame(doku); + this.server.startGame(this.doku, this.gameDurationMinutes.get() * 60); } + ImGui.endDisabled(); + selector.render(); } } - @Override - public void render() { + private void renderPlayers() { ImGui.text("Joueurs :"); { for (Player player : this.client.getGame().getPlayers().values()) { ImGui.bulletText(player.getPseudo()); } } + } + + private void renderTimer() { + ImGui.inputInt("Temps de la partie (minutes)", gameDurationMinutes); + } + + @Override + public void render() { + renderPlayers(); renderGameStatus(); } diff --git a/app/src/main/java/gui/menu/SoloMenu.java b/app/src/main/java/gui/menu/SoloMenu.java index c3b714e..d823671 100644 --- a/app/src/main/java/gui/menu/SoloMenu.java +++ b/app/src/main/java/gui/menu/SoloMenu.java @@ -1,7 +1,8 @@ package gui.menu; -import gui.SudokuSelector; +import gui.widget.SudokuSelector; import imgui.ImGui; +import sudoku.structure.MultiDoku; public class SoloMenu extends BaseView { @@ -9,12 +10,12 @@ public class SoloMenu extends BaseView { public SoloMenu(StateMachine stateMachine) { super(stateMachine); - this.sudokuSelector = new SudokuSelector(true); + this.sudokuSelector = new SudokuSelector(true, "Résoudre le sudoku"); this.sudokuSelector.onSelect.connect(this::pushSudokuState); } - private void pushSudokuState() { - this.stateMachine.pushState(new SudokuView(stateMachine, this.sudokuSelector.getDoku())); + private void pushSudokuState(MultiDoku doku) { + this.stateMachine.pushState(new SudokuView(stateMachine, doku)); } @Override diff --git a/app/src/main/java/gui/menu/StateMachine.java b/app/src/main/java/gui/menu/StateMachine.java index 5af733f..fdf5920 100644 --- a/app/src/main/java/gui/menu/StateMachine.java +++ b/app/src/main/java/gui/menu/StateMachine.java @@ -29,6 +29,11 @@ public class StateMachine { menus.add(menu); } + public void overrideState(BaseView menu) { + menus.getLast().cleanResources(); + menus.set(menus.size() - 1, menu); + } + public void popState() { menus.getLast().cleanResources(); menus.pop(); diff --git a/app/src/main/java/gui/menu/SudokuView.java b/app/src/main/java/gui/menu/SudokuView.java index d75f736..39d01ef 100644 --- a/app/src/main/java/gui/menu/SudokuView.java +++ b/app/src/main/java/gui/menu/SudokuView.java @@ -2,7 +2,7 @@ package gui.menu; import java.util.concurrent.CancellationException; -import gui.SudokuRenderer; +import gui.widget.SudokuRenderer; import imgui.ImGui; import imgui.ImGuiStyle; import sudoku.io.SudokuSerializer; diff --git a/app/src/main/java/gui/widget/LeaderboardRenderer.java b/app/src/main/java/gui/widget/LeaderboardRenderer.java new file mode 100644 index 0000000..20b4c0d --- /dev/null +++ b/app/src/main/java/gui/widget/LeaderboardRenderer.java @@ -0,0 +1,73 @@ +package gui.widget; + +import game.Game; +import game.Player; +import imgui.ImGui; +import imgui.ImVec2; +import imgui.ImVec4; +import imgui.flag.ImGuiCol; +import imgui.flag.ImGuiStyleVar; + +public class LeaderboardRenderer { + + private final Game game; + private final Player currentPlayer; + + private final float cellHeight = 75; + private final ImVec2 cellSize = new ImVec2(12 * cellHeight, cellHeight); + private final ImVec2 rankSize = new ImVec2(cellHeight, cellHeight); + private final ImVec2 scoreSize = rankSize; + private final ImVec2 nameSize = new ImVec2(cellSize.x - cellHeight * 2.0f, cellHeight); + private final ImVec4 cellColorPlayer = new ImVec4(0.20f, 0.67f, 1.0f, 0.5f); + private final ImVec4 cellColorEnemy = new ImVec4(1.0f, 0.0f, 0.0f, 0.5f); + private final int maxPlayersShowed = 2; + + private final int emptyCellCount; + + public LeaderboardRenderer(Game game, Player player) { + this.game = game; + this.currentPlayer = player; + this.emptyCellCount = game.getDoku().getEmptyCells().size(); + } + + private void renderRank(int rank) { + ImGui.button(Integer.toString(rank), rankSize); + } + + private void renderName(String name) { + ImGui.button(name, nameSize); + } + + private void renderScore(int score) { + ImGui.button(Integer.toString(score), scoreSize); + } + + private void renderCell(Player player, int rank, ImVec4 color) { + ImGui.pushStyleColor(ImGuiCol.Button, color); + ImGui.pushStyleColor(ImGuiCol.ButtonHovered, color); + ImGui.pushStyleColor(ImGuiCol.ButtonActive, color); + ImGui.beginChild(player.getPseudo() + "##" + player.getId(), cellSize); + renderRank(rank); + ImGui.sameLine(); + renderName(player.getPseudo()); + ImGui.sameLine(); + renderScore(emptyCellCount - player.getRemainingCells()); + ImGui.endChild(); + ImGui.popStyleColor(3); + } + + public void render() { + var displaySize = ImGui.getIO().getDisplaySize(); + ImGui.setCursorPosX(displaySize.x / 2.0f - cellSize.x / 2.0f); + ImGui.beginChild("Leaderboard", new ImVec2(cellSize.x + 15.0f, cellHeight * maxPlayersShowed)); + ImGui.pushStyleVar(ImGuiStyleVar.ItemSpacing, new ImVec2()); + ImGui.pushStyleVar(ImGuiStyleVar.FrameBorderSize, 3.0f); + for (int i = 0; i < game.getLeaderboard().size(); i++) { + Player player = game.getLeaderboard().get(i); + renderCell(player, i + 1, player == currentPlayer ? cellColorPlayer : cellColorEnemy); + } + ImGui.popStyleVar(2); + ImGui.endChild(); + } + +} diff --git a/app/src/main/java/gui/widget/MultiPlayerCompleteProgress.java b/app/src/main/java/gui/widget/MultiPlayerCompleteProgress.java new file mode 100644 index 0000000..d5bebee --- /dev/null +++ b/app/src/main/java/gui/widget/MultiPlayerCompleteProgress.java @@ -0,0 +1,28 @@ +package gui.widget; + +import game.Game; +import game.Player; +import imgui.ImGui; +import imgui.ImVec2; + +public class MultiPlayerCompleteProgress { + + private final Game game; + private final int emptyCellCount; + private final ImVec2 progressSize = new ImVec2(700, 50); + private final SmoothProgressBar progressBar; + + public MultiPlayerCompleteProgress(Game game) { + this.game = game; + this.emptyCellCount = game.getDoku().getEmptyCells().size(); + this.progressBar = new SmoothProgressBar(); + } + + public void render() { + Player firstPlayer = game.getLeaderboard().getFirst(); + ImGui.setCursorPosX(ImGui.getIO().getDisplaySizeX() / 2.0f - progressSize.x / 2.0f); + String progressText = firstPlayer.getPseudo() + " - " + (emptyCellCount - firstPlayer.getRemainingCells()) + "/" + emptyCellCount; + this.progressBar.render(progressText, progressSize, 1.0f - firstPlayer.getRemainingCells() / (float) emptyCellCount); + } + +} diff --git a/app/src/main/java/gui/widget/SmoothProgressBar.java b/app/src/main/java/gui/widget/SmoothProgressBar.java new file mode 100644 index 0000000..9db2971 --- /dev/null +++ b/app/src/main/java/gui/widget/SmoothProgressBar.java @@ -0,0 +1,21 @@ +package gui.widget; + +import imgui.ImGui; +import imgui.ImVec2; + +public class SmoothProgressBar { + + private float lastProgress = 0; + private final float speed = 2.0f; + private final float clipConstant = 0.001f; + + public void render(String label, ImVec2 size, float progress) { + float delta = progress - lastProgress; + if (Math.abs(delta) < clipConstant) + lastProgress = progress; + else + lastProgress = lastProgress + delta * ImGui.getIO().getDeltaTime() * speed; + ImGui.progressBar(lastProgress, size, label); + } + +} diff --git a/app/src/main/java/gui/SudokuRenderer.java b/app/src/main/java/gui/widget/SudokuRenderer.java similarity index 90% rename from app/src/main/java/gui/SudokuRenderer.java rename to app/src/main/java/gui/widget/SudokuRenderer.java index 7b66580..4b38cf1 100644 --- a/app/src/main/java/gui/SudokuRenderer.java +++ b/app/src/main/java/gui/widget/SudokuRenderer.java @@ -1,4 +1,4 @@ -package gui; +package gui.widget; import java.util.HashMap; import java.util.HashSet; @@ -6,7 +6,13 @@ import java.util.List; import java.util.Map; import java.util.Set; +import common.ConsumerSignal; import common.Signal; +import gui.ColorGenerator; +import gui.Fonts; +import gui.Options; +import gui.RenderableMultidoku; +import gui.Symbols; import gui.ColorGenerator.Color; import imgui.ImGui; import imgui.ImVec2; @@ -33,6 +39,7 @@ public class SudokuRenderer { private final Set diagonals = new HashSet<>(); public final Signal onResolve = new Signal(); + public final ConsumerSignal onCellChange = new ConsumerSignal<>(); public SudokuRenderer(MultiDoku doku) { this.doku = RenderableMultidoku.fromMultidoku(doku); @@ -72,11 +79,13 @@ public class SudokuRenderer { if (currentCell.getSymbolIndex() == i) { if (ImGui.button("X", cellSize)) { currentCell.setSymbolIndex(Cell.NOSYMBOL); + this.onCellChange.emit(currentCell); ImGui.closeCurrentPopup(); } } else { if (ImGui.button(Options.Symboles.getSymbols().get(i), cellSize)) { - currentCell.trySetValue(i); + if (currentCell.trySetValue(i)) + this.onCellChange.emit(currentCell); if (this.doku.getDoku().isSolved()) this.onResolve.emit(); ImGui.closeCurrentPopup(); @@ -98,7 +107,7 @@ public class SudokuRenderer { if (offsetX > 0) { ImGui.setCursorPosX(offsetX); } - ImGui.beginChild(1, new ImVec2(cellSize.x * doku.getWidth(), cellSize.y * doku.getHeight())); + ImGui.beginChild("sudokuChild", new ImVec2(cellSize.x * doku.getWidth(), cellSize.y * doku.getHeight())); ImGui.pushStyleVar(ImGuiStyleVar.FrameBorderSize, 2.0f); ImGui.pushStyleVar(ImGuiStyleVar.ItemSpacing, new ImVec2(0.0f, 0.0f)); diff --git a/app/src/main/java/gui/SudokuSelector.java b/app/src/main/java/gui/widget/SudokuSelector.java similarity index 87% rename from app/src/main/java/gui/SudokuSelector.java rename to app/src/main/java/gui/widget/SudokuSelector.java index 0d46f51..db1b69e 100644 --- a/app/src/main/java/gui/SudokuSelector.java +++ b/app/src/main/java/gui/widget/SudokuSelector.java @@ -1,9 +1,10 @@ -package gui; +package gui.widget; import java.util.ArrayList; import java.util.List; -import common.Signal; +import common.ConsumerSignal; +import gui.SudokuType; import imgui.ImGui; import imgui.extension.imguifiledialog.ImGuiFileDialog; import imgui.extension.imguifiledialog.flag.ImGuiFileDialogFlags; @@ -16,7 +17,7 @@ import sudoku.structure.SudokuFactory; public class SudokuSelector { - public final Signal onSelect = new Signal(); + public final ConsumerSignal onSelect = new ConsumerSignal<>(); private MultiDoku doku; private final boolean canGenEmptyGrid; @@ -26,16 +27,16 @@ public class SudokuSelector { private final ImInt difficulty = new ImInt(Difficulty.Medium.ordinal()); private final List contraints = new ArrayList<>(); - private static final String[] sudokuTypes = { "Carré", "Rectangle", "Multidoku" }; - private static final int SQUARE = 0, RECTANGLE = 1, MULTIDOKU = 2; - private final ImInt sudokuSize = new ImInt(3); private final ImInt sudokuWidth = new ImInt(3); private final ImInt sudokuHeight = new ImInt(3); - public SudokuSelector(boolean canGenEmptyGrid) { + private final String confirmMessage; + + public SudokuSelector(boolean canGenEmptyGrid, String confirmMessage) { this.canGenEmptyGrid = canGenEmptyGrid; + this.confirmMessage = confirmMessage; initConstraints(); } @@ -63,7 +64,7 @@ public class SudokuSelector { e.printStackTrace(); } } - this.onSelect.emit(); + this.onSelect.emit(this.doku); } public void renderFileDialog() { @@ -75,7 +76,7 @@ public class SudokuSelector { String filePath = entry.getValue(); this.doku = SudokuFactory.fromfile(filePath); if (this.doku != null) - this.onSelect.emit(); + this.onSelect.emit(this.doku); } catch (Exception e) { e.printStackTrace(); } @@ -98,7 +99,7 @@ public class SudokuSelector { switch (currentType.getMakerParamCount()) { case 1: ImGui.inputInt("Taille", sudokuSize); - if (ImGui.button("Résoudre un sudoku")) { + if (ImGui.button(confirmMessage)) { selectSudoku(currentType.createDoku(getConstraints(), sudokuSize.get()), false); } if (canGenEmptyGrid && ImGui.button("Générer une grille vide")) { @@ -109,7 +110,7 @@ public class SudokuSelector { case 2: ImGui.inputInt("Largeur", sudokuHeight); ImGui.inputInt("Longueur", sudokuWidth); - if (ImGui.button("Résoudre un sudoku")) { + if (ImGui.button(confirmMessage)) { selectSudoku(currentType.createDoku(getConstraints(), sudokuWidth.get(), sudokuHeight.get()), false); } @@ -129,8 +130,4 @@ public class SudokuSelector { renderFileDialog(); } - public MultiDoku getDoku() { - return doku; - } - } diff --git a/app/src/main/java/gui/widget/TimerRenderer.java b/app/src/main/java/gui/widget/TimerRenderer.java new file mode 100644 index 0000000..10548b7 --- /dev/null +++ b/app/src/main/java/gui/widget/TimerRenderer.java @@ -0,0 +1,29 @@ +package gui.widget; + +import java.time.Instant; + +import imgui.ImGui; + +public class TimerRenderer { + + private final long endTime; + + public TimerRenderer(Instant startTime, long duration) { + this.endTime = startTime.getEpochSecond() + duration; + } + + private long getTimeRemaining() { + long currentTime = Instant.now().getEpochSecond(); + return endTime - currentTime; + } + + public void render() { + long seconds = getTimeRemaining(); + long minutes = seconds / 60; + String text = String.format("%02d:%02d", minutes, seconds % 60); + var textSize = ImGui.calcTextSize(text); + ImGui.setCursorPosX(ImGui.getIO().getDisplaySizeX() / 2.0f - textSize.x / 2.0f); + ImGui.text(text); + } + +} diff --git a/app/src/main/java/network/ConnexionThread.java b/app/src/main/java/network/ConnexionThread.java index 6752d92..17b0584 100644 --- a/app/src/main/java/network/ConnexionThread.java +++ b/app/src/main/java/network/ConnexionThread.java @@ -22,7 +22,7 @@ public class ConnexionThread extends Thread { // System.out.println(objectInputStream.available()); Object o = objectInputStream.readObject(); if (o instanceof Packet packet) { - connexion.visitPacket(packet); + connexion.visit(packet); } } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); diff --git a/app/src/main/java/network/client/Client.java b/app/src/main/java/network/client/Client.java index 10f5ec8..de7865c 100644 --- a/app/src/main/java/network/client/Client.java +++ b/app/src/main/java/network/client/Client.java @@ -4,10 +4,15 @@ import java.io.IOException; import java.net.UnknownHostException; import java.util.Random; +import common.ConsumerSignal; import common.Signal; import game.Game; import game.Player; +import network.protocol.packets.ChangeCellPacket; import network.protocol.packets.LoginPacket; +import sudoku.structure.Cell; +import sudoku.structure.MultiDoku; +import sudoku.structure.Sudoku; public class Client { private final ClientConnexion clientConnection; @@ -17,6 +22,9 @@ public class Client { public final Signal onDisconnect = new Signal(); public final Signal onClosed = new Signal(); public final Signal onGameStarted = new Signal(); + public final ConsumerSignal onGameEnd = new ConsumerSignal<>(); + + Player player; String disconnectReason = null; @@ -54,4 +62,20 @@ public class Client { stop(); } + public void sendCellChange(Cell cell) { + MultiDoku doku = getGame().getDoku(); + for (int sudokuIndex = 0; sudokuIndex < doku.getNbSubGrids(); sudokuIndex++) { + Sudoku sudoku = doku.getSubGrid(sudokuIndex); + int cellIndex = sudoku.getCells().indexOf(cell); + if (cellIndex != -1) { + this.clientConnection.sendPacket(new ChangeCellPacket(sudokuIndex, cellIndex, cell.getSymbolIndex())); + return; + } + } + } + + public Player getPlayer() { + return player; + } + } diff --git a/app/src/main/java/network/client/ClientConnexion.java b/app/src/main/java/network/client/ClientConnexion.java index 6e6fe87..71945ff 100644 --- a/app/src/main/java/network/client/ClientConnexion.java +++ b/app/src/main/java/network/client/ClientConnexion.java @@ -6,19 +6,21 @@ import java.net.UnknownHostException; import game.Player; import network.Connexion; +import network.protocol.packets.ChangeCellPacket; import network.protocol.packets.ConnexionInfoPacket; import network.protocol.packets.DisconnectPacket; +import network.protocol.packets.EndGamePacket; import network.protocol.packets.KeepAlivePacket; import network.protocol.packets.LoginPacket; import network.protocol.packets.PlayerJoinPacket; import network.protocol.packets.PlayerLeavePacket; import network.protocol.packets.StartGamePacket; +import network.protocol.packets.UpdatePlayerScorePacket; import sudoku.io.SudokuSerializer; public class ClientConnexion extends Connexion { private final Client client; - private Player player = null; public ClientConnexion(String address, short port, Client client) throws UnknownHostException, IOException { super(new Socket(address, port)); @@ -35,7 +37,7 @@ public class ClientConnexion extends Connexion { @Override public void visitPacket(ConnexionInfoPacket packet) { - this.player = this.client.getGame().getPlayerById(packet.getConnectionId()); + this.client.player = this.client.getGame().getPlayerById(packet.getConnectionId()); client.onConnect.emit(); } @@ -69,8 +71,28 @@ public class ClientConnexion extends Connexion { @Override public void visitPacket(StartGamePacket packet) { - this.client.getGame().startGame(SudokuSerializer.deserializeSudoku(packet.getSerializedSudoku())); + this.client.getGame().startGame(SudokuSerializer.deserializeSudoku(packet.getSerializedSudoku()), + packet.getInstant(), packet.getGameDuration()); this.client.onGameStarted.emit(); } + @Override + public void visitPacket(EndGamePacket packet) { + Player winner = this.client.getGame().getLeaderboard().getFirst(); + this.client.getGame().stopGame(); + this.client.onGameEnd.emit(winner); + } + + @Override + public void visitPacket(UpdatePlayerScorePacket packet) { + Player player = this.client.getGame().getPlayerById(packet.getPlayerId()); + assert (player != null); + this.client.getGame().setPlayerRemainingCells(player, packet.getCellsLeft()); + } + + @Override + public void visitPacket(ChangeCellPacket packet) { + throw new UnsupportedOperationException("Unimplemented method 'visitPacketChangeCell'"); + } + } diff --git a/app/src/main/java/network/protocol/PacketVisitor.java b/app/src/main/java/network/protocol/PacketVisitor.java index 582a4f3..9639fd5 100644 --- a/app/src/main/java/network/protocol/PacketVisitor.java +++ b/app/src/main/java/network/protocol/PacketVisitor.java @@ -1,16 +1,19 @@ package network.protocol; +import network.protocol.packets.ChangeCellPacket; import network.protocol.packets.ConnexionInfoPacket; import network.protocol.packets.DisconnectPacket; +import network.protocol.packets.EndGamePacket; import network.protocol.packets.KeepAlivePacket; import network.protocol.packets.LoginPacket; import network.protocol.packets.PlayerJoinPacket; import network.protocol.packets.PlayerLeavePacket; import network.protocol.packets.StartGamePacket; +import network.protocol.packets.UpdatePlayerScorePacket; public interface PacketVisitor { - default void visitPacket(Packet packet) { + default void visit(Packet packet) { packet.accept(this); } @@ -21,5 +24,8 @@ public interface PacketVisitor { void visitPacket(PlayerJoinPacket packet); void visitPacket(PlayerLeavePacket packet); void visitPacket(StartGamePacket packet); + void visitPacket(EndGamePacket packet); + void visitPacket(UpdatePlayerScorePacket packet); + void visitPacket(ChangeCellPacket packet); } diff --git a/app/src/main/java/network/protocol/Packets.java b/app/src/main/java/network/protocol/Packets.java index 9355877..6f056e8 100644 --- a/app/src/main/java/network/protocol/Packets.java +++ b/app/src/main/java/network/protocol/Packets.java @@ -2,6 +2,6 @@ package network.protocol; public enum Packets { - ConnectionInfo, KeepAlive, Disconnect, Login, PlayerJoin, PlayerLeave, StartGame + ConnectionInfo, KeepAlive, Disconnect, Login, PlayerJoin, PlayerLeave, StartGame, ChangeCell, EndGame, UpdatePlayerScore } diff --git a/app/src/main/java/network/protocol/packets/ChangeCellPacket.java b/app/src/main/java/network/protocol/packets/ChangeCellPacket.java new file mode 100644 index 0000000..6d1164e --- /dev/null +++ b/app/src/main/java/network/protocol/packets/ChangeCellPacket.java @@ -0,0 +1,38 @@ +package network.protocol.packets; + +import network.protocol.Packet; +import network.protocol.PacketVisitor; +import network.protocol.Packets; + +public class ChangeCellPacket extends Packet { + + static private final long serialVersionUID = Packets.ChangeCell.ordinal(); + + private final int sudokuIndex; + private final int cellIndex; + private final int newValue; + + public ChangeCellPacket(int sudokuIndex, int cellIndex, int newValue) { + this.sudokuIndex = sudokuIndex; + this.cellIndex = cellIndex; + this.newValue = newValue; + } + + public int getSudokuIndex() { + return sudokuIndex; + } + + public int getCellIndex() { + return cellIndex; + } + + public int getNewValue() { + return newValue; + } + + @Override + public void accept(PacketVisitor packetVisitor) { + packetVisitor.visitPacket(this); + } + +} diff --git a/app/src/main/java/network/protocol/packets/EndGamePacket.java b/app/src/main/java/network/protocol/packets/EndGamePacket.java new file mode 100644 index 0000000..42f5066 --- /dev/null +++ b/app/src/main/java/network/protocol/packets/EndGamePacket.java @@ -0,0 +1,18 @@ +package network.protocol.packets; + +import network.protocol.Packet; +import network.protocol.PacketVisitor; +import network.protocol.Packets; + +public class EndGamePacket extends Packet { + + static private final long serialVersionUID = Packets.EndGame.ordinal(); + + public EndGamePacket() { } + + @Override + public void accept(PacketVisitor packetVisitor) { + packetVisitor.visitPacket(this); + } + +} diff --git a/app/src/main/java/network/protocol/packets/StartGamePacket.java b/app/src/main/java/network/protocol/packets/StartGamePacket.java index c8e0ae8..3832240 100644 --- a/app/src/main/java/network/protocol/packets/StartGamePacket.java +++ b/app/src/main/java/network/protocol/packets/StartGamePacket.java @@ -1,5 +1,7 @@ package network.protocol.packets; +import java.time.Instant; + import network.protocol.Packet; import network.protocol.PacketVisitor; import network.protocol.Packets; @@ -9,15 +11,28 @@ public class StartGamePacket extends Packet { static private final long serialVersionUID = Packets.StartGame.ordinal(); private final String serializedSudoku; + // used to resume game + private final Instant instant; + private final long gameDuration; - public StartGamePacket(String serializedSudoku) { + public StartGamePacket(String serializedSudoku, Instant instant, long gameDuration) { this.serializedSudoku = serializedSudoku; + this.instant = instant; + this.gameDuration = gameDuration; } public String getSerializedSudoku() { return serializedSudoku; } + public Instant getInstant() { + return instant; + } + + public long getGameDuration() { + return gameDuration; + } + @Override public void accept(PacketVisitor packetVisitor) { packetVisitor.visitPacket(this); diff --git a/app/src/main/java/network/protocol/packets/UpdatePlayerScorePacket.java b/app/src/main/java/network/protocol/packets/UpdatePlayerScorePacket.java new file mode 100644 index 0000000..f8011df --- /dev/null +++ b/app/src/main/java/network/protocol/packets/UpdatePlayerScorePacket.java @@ -0,0 +1,32 @@ +package network.protocol.packets; + +import network.protocol.Packet; +import network.protocol.PacketVisitor; +import network.protocol.Packets; + +public class UpdatePlayerScorePacket extends Packet { + + static private final long serialVersionUID = Packets.UpdatePlayerScore.ordinal(); + + private final int playerId; + private final int cellsLeft; + + public UpdatePlayerScorePacket(int playerId, int cellsLeft) { + this.playerId = playerId; + this.cellsLeft = cellsLeft; + } + + public int getPlayerId() { + return playerId; + } + + public int getCellsLeft() { + return cellsLeft; + } + + @Override + public void accept(PacketVisitor packetVisitor) { + packetVisitor.visitPacket(this); + } + +} diff --git a/app/src/main/java/network/server/Server.java b/app/src/main/java/network/server/Server.java index 10ea1f8..7c746be 100644 --- a/app/src/main/java/network/server/Server.java +++ b/app/src/main/java/network/server/Server.java @@ -2,12 +2,15 @@ package network.server; import java.io.IOException; import java.net.ServerSocket; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import game.Game; import game.Player; +import game.Game.GameState; import network.protocol.Packet; +import network.protocol.packets.EndGamePacket; import network.protocol.packets.StartGamePacket; import sudoku.io.SudokuSerializer; import sudoku.structure.MultiDoku; @@ -37,7 +40,16 @@ public class Server { } } - public void update() { + private void checkTimer() { + if (getGame() == null || getGame().getGameState() != GameState.GameGoing) + return; + long now = Instant.now().getEpochSecond(); + long end = getGame().getStartTime().getEpochSecond() + getGame().getGameDuration(); + if (now > end) + stopGame(); + } + + private void checkConnexions() { for (var it = connexions.iterator(); it.hasNext();) { ServerConnexion connexion = it.next(); if (!connexion.update()) { @@ -48,6 +60,11 @@ public class Server { } } + public void update() { + checkTimer(); + checkConnexions(); + } + public void stop() { this.acceptThread.cancel(); this.logicThread.cancel(); @@ -68,9 +85,19 @@ public class Server { return game; } - public void startGame(MultiDoku doku) { - this.game.startGame(doku); - broadcastPacket(new StartGamePacket(SudokuSerializer.serializeSudoku(doku).toString())); + public void startGame(MultiDoku doku, long gameDuration) { + Instant now = Instant.now(); + this.game.startGame(doku, now, gameDuration); + for (ServerConnexion connexion : this.connexions) { + connexion.setSudoku(doku.clone()); + } + broadcastPacket(new StartGamePacket(SudokuSerializer.serializeSudoku(doku).toString(), now, gameDuration)); + } + + public void stopGame() { + // we don't need to specify the winner since it has to be the first + broadcastPacket(new EndGamePacket()); + getGame().stopGame(); } } diff --git a/app/src/main/java/network/server/ServerConnexion.java b/app/src/main/java/network/server/ServerConnexion.java index 20879a7..68de01b 100644 --- a/app/src/main/java/network/server/ServerConnexion.java +++ b/app/src/main/java/network/server/ServerConnexion.java @@ -3,17 +3,23 @@ package network.server; import java.io.IOException; import java.net.Socket; +import game.Game; import game.Player; import game.Game.GameState; import network.Connexion; +import network.protocol.packets.ChangeCellPacket; import network.protocol.packets.ConnexionInfoPacket; import network.protocol.packets.DisconnectPacket; +import network.protocol.packets.EndGamePacket; import network.protocol.packets.KeepAlivePacket; import network.protocol.packets.LoginPacket; import network.protocol.packets.PlayerJoinPacket; import network.protocol.packets.PlayerLeavePacket; import network.protocol.packets.StartGamePacket; +import network.protocol.packets.UpdatePlayerScorePacket; import sudoku.io.SudokuSerializer; +import sudoku.structure.Cell; +import sudoku.structure.MultiDoku; public class ServerConnexion extends Connexion { @@ -21,6 +27,7 @@ public class ServerConnexion extends Connexion { private final KeepAliveHandler keepAliveHandler; private boolean shouldClose = false; private Player player = null; + private MultiDoku doku; public ServerConnexion(Socket socket, Server server) throws IOException { super(socket); @@ -29,7 +36,7 @@ public class ServerConnexion extends Connexion { } public boolean update() { - if (shouldClose | isClosed()) + if (shouldClose || isClosed()) return false; return this.keepAliveHandler.update(); } @@ -44,7 +51,7 @@ public class ServerConnexion extends Connexion { @Override public synchronized void close() { - if(shouldClose) + if (shouldClose) return; super.close(); shouldClose = true; @@ -54,13 +61,22 @@ public class ServerConnexion extends Connexion { private void finishLogin() { // send players that have already joined (excluding this one) for (Player p : this.server.getGame().getPlayers().values()) { - if (p.getId() != player.getId()) + if (p.getId() != player.getId()) { sendPacket(new PlayerJoinPacket(p)); + sendPacket(new UpdatePlayerScorePacket(p.getId(), p.getRemainingCells())); + } } + this.server.broadcastPacket(new PlayerJoinPacket(player)); sendPacket(new ConnexionInfoPacket(player.getId())); - if (this.server.getGame().getGameState() == GameState.GameGoing) { - sendPacket(new StartGamePacket(SudokuSerializer.serializeSudoku(this.server.getGame().getDoku()).toString())); + + Game game = this.server.getGame(); + + if (game.getGameState() == GameState.GameGoing) { + setSudoku(game.getDoku().clone()); + sendPacket( + new StartGamePacket(SudokuSerializer.serializeSudoku(game.getDoku()).toString(), + game.getStartTime(), game.getGameDuration())); } } @@ -102,4 +118,50 @@ public class ServerConnexion extends Connexion { throw new UnsupportedOperationException("Unimplemented method 'visitPacketStartGame'"); } + @Override + public void visitPacket(EndGamePacket packet) { + throw new UnsupportedOperationException("Unimplemented method 'visitPacket'"); + } + + @Override + public void visitPacket(UpdatePlayerScorePacket packet) { + throw new UnsupportedOperationException("Unimplemented method 'visitPacket'"); + } + + @Override + public void visitPacket(ChangeCellPacket packet) { + Cell cell = this.doku.getSubGrid(packet.getSudokuIndex()).getCell(packet.getCellIndex()); + if (cell.getSymbolIndex() == Cell.NOSYMBOL && packet.getNewValue() == Cell.NOSYMBOL) + return; + if (cell.getSymbolIndex() != Cell.NOSYMBOL && packet.getNewValue() != Cell.NOSYMBOL) { + cell.trySetValue(packet.getNewValue()); + return; + } + if (cell.getSymbolIndex() != Cell.NOSYMBOL && packet.getNewValue() == Cell.NOSYMBOL) { + cell.empty(); + this.server.getGame().setPlayerRemainingCells(player, player.getRemainingCells() + 1); + this.server.broadcastPacket(new UpdatePlayerScorePacket(player.getId(), player.getRemainingCells())); + return; + } + // on rajoute un chiffre à la grille + if (cell.trySetValue(packet.getNewValue())) { + this.server.getGame().setPlayerRemainingCells(player, player.getRemainingCells() - 1); + this.server.broadcastPacket(new UpdatePlayerScorePacket(player.getId(), player.getRemainingCells())); + } + checkWin(); + } + + private void checkWin() { + if (this.player.getRemainingCells() == 0) { + this.server.stopGame(); + } + } + + public void setSudoku(MultiDoku doku) { + this.doku = doku; + assert (player != null); + this.server.getGame().setPlayerRemainingCells(player, this.doku.getEmptyCells().size()); + this.server.broadcastPacket(new UpdatePlayerScorePacket(player.getId(), player.getRemainingCells())); + } + } diff --git a/app/src/main/java/network/server/ServerLogicThread.java b/app/src/main/java/network/server/ServerLogicThread.java index c9b2a4b..d238ca2 100644 --- a/app/src/main/java/network/server/ServerLogicThread.java +++ b/app/src/main/java/network/server/ServerLogicThread.java @@ -19,7 +19,7 @@ public class ServerLogicThread extends Thread { try { Thread.sleep(50); } catch (InterruptedException e) { - // e.printStackTrace(); + e.printStackTrace(); break; } } diff --git a/app/src/main/java/sudoku/structure/Cell.java b/app/src/main/java/sudoku/structure/Cell.java index eaa4964..b6127ad 100644 --- a/app/src/main/java/sudoku/structure/Cell.java +++ b/app/src/main/java/sudoku/structure/Cell.java @@ -126,6 +126,8 @@ public class Cell { } public boolean trySetValue(int newValue) { + if (!isMutable()) + return false; if (!canHaveValue(newValue)) return false; setSymbolIndex(newValue); diff --git a/app/src/main/java/sudoku/structure/MultiDoku.java b/app/src/main/java/sudoku/structure/MultiDoku.java index eb64d46..d817eb7 100644 --- a/app/src/main/java/sudoku/structure/MultiDoku.java +++ b/app/src/main/java/sudoku/structure/MultiDoku.java @@ -1,6 +1,10 @@ package sudoku.structure; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; import sudoku.io.SudokuSerializer; @@ -178,4 +182,18 @@ public class MultiDoku { int randomIndex = rand.nextInt(emptyCells.size()); return emptyCells.get(randomIndex); } + + public void clearMutableCells() { + for (Sudoku s : getSubGrids()) { + for (Cell cell : s.getCells()) { + if (cell.isMutable()) + cell.clearCurrentSymbol(); + } + } + } + + public MultiDoku clone() { + //TODO: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah + return SudokuSerializer.deserializeSudoku(SudokuSerializer.serializeSudoku(this)); + } }